一、网络编程套接字
1.1 基础概念
【网络编程】指网络上的主机,通过不同的进程,以编程的方式实现网络通信;当然,我们只要满足进程不同就行,所以即便是同一个主机,只要是不同进程,基于网络来传输数据,也属于网络编程
【套接字】其实是socket的直译,套接字就是传输层给应用层提供的网络编程API(接口)通过这个接口,应用层程序可以通过这个接口使用传输层提供的服务,而不需要知道它的具体实现
套接字分为两类:流式套接字和数据报套接字
流式套接字是基于TCP协议(一个传输层协议)实现的,TCP是一种面向连接型、可靠传输型、面向字节流、全双工的传输层协议,流式套接字利用这些特性为应用层提供了一个简单的接口,用于发送和接收数据流
数据报套接字是基于UDP协议(也是一个传输层协议)实现的,UDP是一种面向无连接型、不可靠传输型、面向数据报、全双工的传输层协议
1.2 协议特点
接下来讲解以下上述提到的 TCP协议和UDP协议的特点
1)面向有连接型 vs 面向无连接型:通过网络发送数据分为面向有连接和面向无连接
有连接指在发送数据之前,发送端要先和接收端建立一条逻辑意义上的连接,连接建好后才能真正发送数据,数据发送完毕后要断开连接;
就好比打电话,在说话之前,对方要先同意接听,接听并说完话后再挂断电话
无连接则无需考虑建立连接和断开连接,发送端可以在任何时候发送数据,接收端不知道自己会在何时收到数据,所以要时常检查是否收到数据;
这个就像发送电子邮件,发送端可以随时发送,无需让接收端同意,接收端则要时常检查是否有收到邮件
2)可靠传输 vs 不可靠传输:
可靠传输指将要传输的数据尽可能的传输给对方,在网络通信的过程中,会存在"丢包"的情况:A给B传输10个数据报,B收到了9个;
原因是A传输给B,中间可能会经历很多交换机和路由器,这些交换机和路由器不只是转发你的数据,要转发很多数据,当数据很多时,可能会超过它们自身的硬件水平,此时多出来的数据无法转发,会被直接丢弃掉。
TCP为了对抗丢包,内部实现了一些机制(重发)来实现可靠传输(机制后面会详细讲)
不可靠传输指再出现丢包后,也不负责重发,不可靠传输更注重效率,在一些注重效率,对准确性要求不高的场景使用不可靠传输,可靠传输能尽可能保证数据传给接收端,但效率上会大打折扣
3)面向字节流 vs 面向数据报
面向字节流指传输的数据以字节为单位
面向数据报指传输的数据以数据报为单位,传输数据是一个一个数据报,一次读写只能读写完整的数据报,不能读写半个
4)全双工 vs 半双工
全双工指一条链路,能够进行双向通信,后续代码创建socket对象,既可以读(接收)也可以写(发送)
半双工指一条链路,只能进行单向通信
二、UDP-数据报套接字编程
socket API 是由传输层给应用层提供的API,传输层是封装于操作系统内核态的,由操作系统内核直接管理,所以可以理解为socket api是由操作系统内核管理的,而Java对于系统这些API进行了封装,所以使得用户程序可以直接使用这些API
UDP的socket API 有两个重要的类
2.1 DatagramSocket
属于UDP Socket,创建DatagramSocket的对象就可以发送和接收UDP数据报,先来看构造方法:
构造方法 | 描述 |
DatagramSocket( ) | 创建一个UDP数据报套接字的Socket,绑定到本机任意一个随机端口 |
DatagramSocket( int port ) | 创建一个UDP数据报套接字的Socket,绑定到本机指定的端口号 |
普通方法:
普通方法 | 描述 |
void receive (DatagramPacket p) | 接收数据报,如果没有接收到,该方法就会阻塞等待 |
void send(DatagramPacket p) | 发送数据报,不会阻塞等待,直接发送(无连接) |
void close( ) | 关闭此数据报套接字 |
当创建一个套接字时,系统会为其分配资源绑定端口号,如果用完不关闭则会导致资源持续被占用
2.2 DatagramPacket
表示UDP Socket发送和接收的数据报,一个DatagramPacket对象就相当于一个UDP数据报
构造方法 | 描述 |
DatagramPacket (byte[] buf, int length) | 构造⼀个DatagramPacket以用来接收数据报,接收的数据保存在字节数组(第⼀个参数buf)中,接收指定长度(第⼆个参数length) |
DatagramPacket(byte[] buf, int offset, int length, SocketAddress address) | 构造⼀个DatagramPacket以用来发送数据报,发送的数据为字节数组(第⼀个参数buf)中,从0到指定⻓ 度(第⼆个参数length)address指定⽬的主机的IP 和端⼝号 |
上述方法可以结合下述代码理解
2.3 模拟回显服务器
回显服务器指客户端发送一个请求给服务端,服务端将这个请求原封不动的作为相应返回给客户端,这就叫回显(请求啥相应就是啥),接下来先编写服务器程序:
java">public class UdpEchoServer {public DatagramSocket socket = null;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port); //创建 UDP Socket 并绑定一个端口号}}
服务器需要在程序启动的时候,把在服务器程序的端口号确定下来,客户端发送请求时需要知道服务器的IP地址(服务器所在主机的IP)、端口号port
服务器要能够接收客户端发送的数据,socket receive( );需要向receive传入一个UDP数据报
java">public class UdpEchoServer {public DatagramSocket socket = null;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}public void start() throws IOException {//1) 接收请求DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);//此时创建好的requestPacket 是一个空的数据包//requestPacket包含两个部分1.报头 2.载荷//字节数组用来存储数据socket.receive(requestPacket); //客户端会send一个数据包, 就会跳转到这里//此时由requestPacket 是一个预留好空间的空数据包// 为了方便在 java 代码中处理 (尤其是后面进行打印) 可以把上述数据报中的二进制数据, 拿出来, 构造成 StringString request = new String(requestPacket.getData(), 0, requestPacket.getLength());}}
接收来自客户端的请求后,经过处理后将响应返回给客户端,那么该如何知道应该给哪个客户端返回响应,在我们receive接收到的数据包里就包含了这个数据包来自于哪个IP,来自于哪个端口号(客户端)
java">// 2) 根据请求计算响应
String response = this.process(request);
// 3) 把响应写回到客户端
DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), 0, response.getBytes().length, requestPacket.getSocketAddress());
socket.send(responsePacket);
requestPacket.getSocketAddress() 这个方法返回的对象里就包含了客户端的IP地址和端口号
上述代码干的事情就是将字符串类型的二进制数据再构造会UDP数据包并发送给客户端
服务端完整代码如下:
java">public class UdpEchoServer {private DatagramSocket socket = null;public UdpEchoServer(int port) throws SocketException {socket = new DatagramSocket(port);}public void start() throws IOException {Scanner scanner = new Scanner(System.in);System.out.println("服务器启动!");while (true) { // 服务器需要7*24小时持续接收并处理请求// 1) 读取请求并解析DatagramPacket requestPacket = new DatagramPacket(new byte[4096], 4096);socket.receive(requestPacket);//receive方法中的requestPacket是一个空的数据包,客户端程序通过send方法发送有数据的数据包后//会直接跳转到这里的receive方法,而这里的requestPacket是一个预留好空间的空数据包// 为了方便在 java 代码中处理 (尤其是后面进行打印) 可以把上述数据报中的二进制数据, 拿出来, 构造成 StringString request = new String(requestPacket.getData(), 0, requestPacket.getLength());//将字节数组构造成String类的对象// 2) 根据请求计算响应String response = this.process(request);// 3) 把响应写回到客户端DatagramPacket responsePacket = new DatagramPacket(response.getBytes(), 0, response.getBytes().length,requestPacket.getSocketAddress());socket.send(responsePacket);System.out.printf("[%s:%d] req=%s, resp=%s\n", requestPacket.getAddress(), requestPacket.getPort(),request, response); // 从左到右依次为: IP地址,端口号,请求,响应}}// 由于当前写的是 "回显服务器"public String process(String request) {return request;}public static void main(String[] args) throws IOException {UdpEchoServer server = new UdpEchoServer(9090);server.start();}
}
接下来写客户端代码:
首先客户端需要知道服务器的IP和端口号,端口号是我们之前就设置的9090,IP用127.0.0.1,当服务器和客户端在一个主机上,就用环回IP,这是系统提供的特殊的IP
java">public class UdpEchoClient {private DatagramSocket socket = null;private String serverIp;private int serverPort;public UdpEchoClient(String serverIp, int serverPort) throws SocketException {socket = new DatagramSocket();// 这俩信息需要额外记录下来, 以备后续使用.this.serverIp = serverIp;this.serverPort = serverPort;}
}
上述构造socket对象没有指定端口号,这样操作系统会分配一个空闲的端口号,这个端口号每次重新启动程序都不一样
java">// 1. 从控制台读取用户输入
String request = scanner.next();
// 2. 构造请求并发送
// 构造请求数据报的时候, 不光要有数据, 还要有 "目标 "
DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), 0, request.getBytes().length, InetAddress.getByName(serverIp), serverPort);
socket.send(requestPacket); //发送数据包
上述InetAddress.getByName(serverIp)是将字符串格式的IP地址转成Java能识别的InetAddress对象
发送完数据包,服务器经过处理返回响应,客户端就要接收响应
java">// 3. 读取响应数据
//构造一个空的数据包负责接收服务器返回的响应
DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);
// 4. 显示响应到控制台上.
String response = new String(responsePacket.getData(), 0, responsePacket.getLength());
System.out.println(response);
在第2步执行完send后,客户端程序紧接着到第三步的receive,由于从发送请求到返回响应需要些时间,所以这里receive会阻塞,阻塞到接收到服务器返回响应
完整的客户端代码如下:
java">public class UdpEchoClient {private DatagramSocket socket = null;private String serverIp;private int serverPort;public UdpEchoClient(String serverIp, int serverPort) throws SocketException {socket = new DatagramSocket();// 这俩信息需要额外记录下来, 以备后续使用.this.serverIp = serverIp;this.serverPort = serverPort;}public void start() throws IOException {System.out.println("客户端启动!");Scanner scanner = new Scanner(System.in);while (true) {System.out.print("请输入要发送的请求: ");// 1. 从控制台读取用户输入String request = scanner.next();// 2. 构造请求并发送// 构造请求数据报的时候, 不光要有数据, 还要有 "目标"DatagramPacket requestPacket = new DatagramPacket(request.getBytes(), 0, request.getBytes().length,InetAddress.getByName(serverIp), serverPort);socket.send(requestPacket);// 3. 读取响应数据DatagramPacket responsePacket = new DatagramPacket(new byte[4096], 4096);socket.receive(responsePacket);// 4. 显示响应到控制台上.String response = new String(responsePacket.getData(), 0, responsePacket.getLength());System.out.println(response);}}public static void main(String[] args) throws IOException {UdpEchoClient client = new UdpEchoClient("127.0.0.1", 9090);// UdpEchoClient client = new UdpEchoClient("139.155.74.81", 9090);client.start();}
}
接下来启动服务器程序和客户端程序:
客户端可以不断发送请求并得到响应
服务端会不断处理客户端的请求