丰富的网络资源,用户在浏览器中,打开在线视频网站,如优酷看视频,实质是通过网络,获取到网络上的一个视频资源。
与本地打开视频文件类似,只是视频文件这个资源的来源是网络。
相比本地资源来说,网络提供了更为丰富的网络资源:
所谓的网络资源,其实就是在网络中可以获取的各种数据资源。
而所有的网络资源,都是通过网络编程来进行数据传输的
网络编程,指网络上的主机,通过不同的进程,以编程的方式实现网络通信(或称为网络数据传输)。
当然,我们只要满足进程不同就行;所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程。
特殊的,对于开发来说,在条件有限的情况下,一般也都是在一个主机中运行多个进程来完成网络编程。
但是,我们一定要明确,我们的目的是提供网络上不同主机,基于网络来传输数据资源:
(1)发送端和接收端
在一次网络数据传输时:
发送端:数据的发送方进程,称为发送端。发送端主机即网络通信中的源主机。
接收端:数据的接收方进程,称为接收端。接收端主机即网络通信中的目的主机。
收发端:发送端和接收端两端,也简称为收发端。
注意:发送端和接收端只是相对的,只是一次网络数据传输产生数据流向后的概念。
(2)请求和响应
(3)客户端和服务端
服务端:在常见的网络数据传输场景下,把提供服务的一方进程,称为服务端,可以提供对外服务。
客户端:获取服务的一方进程,称为客户端。
对于服务来说,一般是提供:
(4)常见的客户端服务器模型
最常见的场景,客户端是指给用户使用的程序,服务端是提供用户服务的程序:
(5) 客户端和服务器之间的交互方式
(6)进行网络编程需要使用操作系统提供的API
传输层提供了网络通信的API,这些API也叫做socket API,操作系统提供的原生API,是C语言的,因此JVM就把C风格的socket api 封装了一下,变成了Java中面向对象风格的api
Socket套接字,是系统提供于网络通信的技术,是基于TCP/IP协议的网络通信的基本操作单元。基于Socket套接字的网络程序开发就是网络编程
Socket套接字主要针对传输层协议划分为如下三类:
(1)流套接字:使用传输层TCP协议
TCP的特点:
- 有连接(类似于打电话,先建立连接,再进行通话)
- 可靠传输(发送的数据对方收没收到,发送方有感知)
- 面向字节流
- 有接收和发送缓冲区
- 大小不限
对于字节流来说,可以简单的理解为,传输数据是基于IO流,流式数据的特征就是在IO流没有关闭的情况下,是无边界的数据,可以多次发送,也可以分开多次接收。
(2)数据报套接字:使用传输层UDP协议
UDP的特点
- 无连接(类似于发微信,不必建立连接,可直接发送消息)
- 不可靠传输(发送的数据对方收没收到,发送方并不关心)
- 面向数据报
- 有接受缓冲区,无发送缓冲区
- 大小受限:依次最多传输64k
对于数据报来说,可以简单的理解为,传输数据是一块一块的,发送一块数据假如100个字节,必须一次发送,接收也必须一次接收100个字节,而不能分100次,每接接收1个字节。
(3)原始套接字
原始套接字用于自定义传输层协议,用于读写内核没有处理的IP协议数据
对于UDP协议来说,具有无连接,面向数据报的特征,即每次都是没有建立连接,并且一次发送全部数
据报,一次接收全部的数据报。
java中使用UDP协议通信,主要基于 DatagramSocket 类来创建数据报套接字,并使用DatagramPacket 作为发送或接收的UDP数据报。对于一次发送及接收UDP数据报的流程如下:
以上只是一次发送端的UDP数据报发送,及接收端的数据报接收,并没有返回的数据。也就是只有请
求,没有响应。对于一个服务端来说,重要的是提供多个客户端的请求处理及响应,流程如下:
DatagramSocket API
DatagramSocket 是UDP Socket,用于发送和接收UDP数据报
DatagramSocket构造方法:
DatagramSocket 方法:
【补充】:
- DatagramSocket 属于是socket类,本质上相当于是一个"文件",在系统有一个种特殊的socket文件,这种文件对应到网卡设备,此时构造一个DatagramSocket对象, 就相当于是打开了一个内核中的socket文件
- 打开socket文件之后,就能进行传输数据了
DatagramPacket API
DatagramPacket 是UDP Socket发送和接收的数据报
DatagramPacket 的构造方法:
DatagramPacket 方法:
构造UDP发送的数据报时,需要传入 SocketAddress ,该对象可以使用 InetSocketAddress 来创建。
InetSocketAddress API
InetSocketAddress ( SocketAddress 的子类 )构造方法:
【关于端口号】:
在操作系统中端口号的范围是0-65535,程序如果想要进行网络通信,就需要获取到一个端口号,端口号相当于用来在网络上区分进程的身份标识符(操作系统收到网卡的数据,就可以根据网络数据报中的端口号来决定要将这个数据交给哪个进程)
分配端口号有两种方式:程序员手动指定和系统自动分配
【UDP版本的回显服务器-客户端】(echo server)
服务器代码:
package network;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.SocketException;
public class UdpEchoServer {
private DatagramSocket socket=null;
public UdpEchoServer(int port) throws SocketException {
//绑定一个端口,把这个服务器进程与一个端口号关联起来
socket=new DatagramSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动:");
while (true){
//去读客户端发来的请求
DatagramPacket requsetPacket=new DatagramPacket(new byte[4096],4096);
socket.receive(requsetPacket);
//解析请求,根据DatagramPacket构造String
String request=new String(requsetPacket.getData(),0, requsetPacket.getLength());
//根据请求,构造响应
String response=process(request);
//将响应构造成DatagramPacket对象
//response.getBytes().length不能写成response.length()
//因为字节数组的长度不一定和字符串的长度相等,当字符串中包含中文字符时,两个长度一定是不相等的
DatagramPacket responsePacket=new DatagramPacket(response.getBytes(),0,response.getBytes().length
,requsetPacket.getSocketAddress());
//requsetPacket.getSocketAddress()获取客户端的IP+端口
//将响应放回给客户端
socket.send(responsePacket);
System.out.printf("[%s:%d] res=%s;resp=%s\n",requsetPacket.getAddress().toString(),
requsetPacket.getPort(),request,response);
}
}
//根据请求计算响应的方法
public String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
UdpEchoServer udpEchoServer=new UdpEchoServer(8000);
udpEchoServer.start();
}
}
客户端代码:
package network;
import java.io.IOException;
import java.net.*;
import java.util.Scanner;
public class UdpEchoClient {
private DatagramSocket socket=null;
public UdpEchoClient() throws SocketException {
//客户端的端口号,一般都是有操作系统自动分配
socket= new DatagramSocket();
}
public void start() throws IOException {
Scanner scanner=new Scanner(System.in);
while (true){
System.out.println(">");
//由用户在控制台输入一个请求数据
String request=scanner.next();
//根据String 构造一个DatagramPacket对象
DatagramPacket requestPacket=new DatagramPacket(request.getBytes(),request.getBytes().length,
InetAddress.getByName("127.0.0.1"),8000);
//将DatagramPacket对象发送给服务器
socket.send(requestPacket);
//从服务器读取响应数据
DatagramPacket responsePacket=new DatagramPacket(new byte[4096],4096);
socket.receive(responsePacket);
String response=new String(responsePacket.getData(),0, responsePacket.getLength());
System.out.printf("req:%s;resp:%s\n",request,response);
}
}
public static void main(String[] args) throws IOException {
UdpEchoClient client=new UdpEchoClient();
client.start();
}
}
【服务器端口号和客户端端口号】
服务器,端口号一般都是手动指定的,如果由操作系统自动分配,客户端就不知道服务器的端口号是多少,也就无法向客户端发送请求了
客户端,端口一般是自动分配的,因为客户端程序是安装在用户电脑上的,用户电脑当前运行哪些程序,是不可控的,如果要是手动指定端口,就可能发生端口冲突
【翻译服务器】(英译汉)
服务器代码:
package network;
import java.io.IOException;
import java.net.SocketException;
import java.security.PrivateKey;
import java.util.HashMap;
public class UdpDictServer extends UdpEchoServer {
private HashMap<String,String> dict=new HashMap<>();
public UdpDictServer(int port) throws SocketException {
super(port);
dict.put("cat","小猫");
dict.put("dog","小狗");
dict.put("fuck","卧槽");
}
@Override
public String process(String request) {
return dict.getOrDefault(request,"这个俺也不会");
}
public static void main(String[] args) throws IOException {
UdpDictServer server=new UdpDictServer(8000);
server.start();
}
}
客户端代码同上
ServerSocket API
ServerSocket是创建TCP服务端的Socket的API
ServerSocket构造方法
ServerSocket 方法:
Socket API
Socket 是客户端Socket,或服务端中接收到客户端建立连接(accept方法)的请求后,返回的服务端Socket。
不管是客户端还是服务端Socket,都是双方建立连接以后,保存的对端信息,及用来与对方收发数据的。
- accept 方法没有参数,返回值是Socket对象,功能是等待有客户端和服务器建立连接,accept会把这个连接获取到进程中,进一步的通过返回值的Socket对象和客户端进行交互
- Socket:服务器和客户端都会使用Socket,通过Socket对象,就可以进行发送/接收数据了,这里的传输数据,不是直接通过Socket对象,而是Socket内部包含了输入流(接收)和输出流(发送)对象
【举例理解ServerSocket和Socket的作用】
对于售楼来说,是有明确的分工的,有人是负责在外场拉客的,有人负责为拉到的客人提供服务
ServerSocket起到的作用就相当于是外场拉客
Socket起到的作用就相当于在内场提供服务
【TCP中的长连接与短链接】
TCP发送数据时,需要先建立连接,什么时候关闭连接就决定是短连接还是长连接。
短连接:每次接收到数据并返回响应后,都关闭连接,即是短连接。也就是说,短连接只能一次收发数据。
长连接:不关闭连接,一直保持连接状态,双方不停的收发数据,即是长连接。也就是说,长连接可以多次收发数据。
区别:
- 建立连接、关闭连接的耗时:短连接每次请求、响应都需要建立连接,关闭连接;而长连接只需要第一次建立连接,之后的请求、响应都可以直接传输。相对来说建立连接,关闭连接也是要耗时的,长连接效率更高。
- 主动发送请求不同:短连接一般是客户端主动向服务端发送请求;而长连接可以是客户端主动发送请求,也可以是服务端主动发。
- 两者的使用场景有不同:短连接适用于客户端请求频率不高的场景,如浏览网页等。长连接适用于客户端与服务端通信频繁的场景,如聊天室,实时游戏等。
基于BIO(同步阻塞IO)的长连接会一直占用系统资源。对于并发要求很高的服务端系统来说,这样的消耗是不能承受的。
由于每个连接都需要不停的阻塞等待接收数据,所以每个连接都会在一个线程中运行。
一次阻塞等待对应着一次请求、响应,不停处理也就是长连接的特性:一直不关闭连接,不停的处理请求。
实际应用时,服务端一般是基于NIO(即同步非阻塞IO)来实现长连接,性能可以极大的提升。
【TCP版本的回显服务器-客户端】(echo server)
服务器代码:
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoServer {
ServerSocket serverSocket=null;
public TcpEchoServer(int port) throws IOException {
serverSocket=new ServerSocket(port);
}
public void start() throws IOException {
System.out.println("服务器启动:");
while (true){
Socket clientSocket=serverSocket.accept();
processConnect(clientSocket);
}
}
//长连接的版本
private void processConnect(Socket clinetSocket) throws IOException {
System.out.printf("[%s %d] 建立连接\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
try(InputStream inputStream=clinetSocket.getInputStream();
OutputStream outputStream= clinetSocket.getOutputStream();){
Scanner scanner=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
if(!scanner.hasNext()){
System.out.printf("[%s %d] 断开连接\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort());
break;
}
//1.读取请求并解析
String request=scanner.next();
//2.根据请求计算相应
String response=process(request);
//3.将响应写回给客户端
printWriter.println(response);
printWriter.flush();
System.out.printf("[%s %d] req:%s;resp:%s\n",clinetSocket.getInetAddress().toString(),clinetSocket.getPort(),
request,response);
}
}
}
private String process(String request){
return request;
}
public static void main(String[] args) throws IOException {
TcpEchoServer server=new TcpEchoServer(8000);
server.start();
}
}
客户端代码:
package network;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PrintWriter;
import java.net.Socket;
import java.util.Scanner;
public class TcpEchoClient {
private Socket socket=new Socket();
public TcpEchoClient() throws IOException {
//需要和服务器建立连接
//客户端new Socket的同时就是在与服务器建立连接的过程
//需要指定服务器的IP地址和端口号
socket=new Socket("127.0.0.1",8000);
}
public void start() throws IOException {
//通过循环来实现长连接
Scanner scanner=new Scanner(System.in);
try(InputStream inputStream=socket.getInputStream();
OutputStream outputStream= socket.getOutputStream()){
Scanner scanner1=new Scanner(inputStream);
PrintWriter printWriter=new PrintWriter(outputStream);
while (true){
System.out.println(">");
//1.从控制台读取用户的请求
String request=scanner.next();
//2.将请求发送给服务器
printWriter.println(request);
printWriter.flush();
//3.接收服务器的响应
String response=scanner1.next();
//4.将结果显示到界面上
System.out.printf("req:%s;resp:%s\n",request,response);
}
}
}
public static void main(String[] args) throws IOException {
TcpEchoClient client=new TcpEchoClient();
client.start();
}
}
【注意细节】