【Java网络编程02】套接字编程
1. Socket套接字
概念:Socket套接字,就是系统提供用于实现网络通信的技术,是基于TCP/IP协议的网络通信基本操作单元。基于Socket套接字的网络程序开发就是网络编程。
分类:
我们可以把Socket套接字分为两类
- 流套接字:使用传输层TCP协议
TCP,即Transmission Control Protocol(传输控制协议)
以下为TCP的特点:(细节后续有专门章节解释)
- 有连接的
- 可靠传输
- 面向字节流的
- 全双工的
- 数据报套接字:使用传输层UDP协议
UDP,即User Datagram Protocol(用户数据报协议)
一下为UDP的特点(细节后续有专门章节解释)
- 无连接的
- 不可靠传输
- 面向数据报
- 全双工
这里简单介绍一些相关概念:
面向字节VS面向数据报:
这里与文件流中的字符流与字节流很类似,面向字节表明网络传输数据是以字节为单位的,而面向数据报表明UDP传输依靠UDP数据报进行传输(稍后我们在代码中会体现)
全双工VS半双工:
半双工:通信双方基于管道进行传输,但是数据只能单向流动,如图所示:
全双工:通信双方可以实现数据的双向流动,如图所示:
2. UDP数据报套接字编程
2.1 相关API
在运用UDP进行网络编程之前,我们需要先熟悉UDP套接字编程相关API的使用,只有掌握了这些API工具才能更好地进行编程的实现,我们主要学习的有两个类:DatagramSocket,DatagramPacket
- DatagramSocket:OS提供了网络编程所需的API,也叫做"Socket API",而Java又进行了一层封装,使用提供的类DatagramSocket就可以实现对于网卡等硬件设备文件的读写操作。
- DatagramPacket:前面我们有介绍过,UDP协议是面向数据报的,因此网络传输单位不是字节而是数据报,Java提供类DatagramPacket相当于数据报的抽象,因此实例化该对象相当于构建了一个数据报。在编程中我们发送的与接收数据的参数就是DatagramPacket对象
DatagramSocket(列举部分):
修饰符+返回类型 | 签名 | 说明 |
---|---|---|
构造方法 | DatagramSocket() | 无参构造,创建一个实例对象(通常用于客户端) |
构造方法 | DatagramSocket(int port) | 含参构造,参数为端口号,创建一个实例对象(通常用于服务器端) |
void | send(DatagramPacket p) | 向socket中发送一个数据报 |
void | receive(DatagramPacket p) | 从socket中接收一个数据报(接收不到就阻塞等待) |
DatagramPacket(列举部分):
修饰符+返回类型 | 签名 | 说明 |
---|---|---|
构造方法 | DatagramPacket(byte[] buf, int length) | 构建一个用于接收数据长度为length的数据报对象 |
构造方法 | DatagramPacket(byte[] buf, int length, InetAddress address, int port) | 构建一个将要发送的数据长度为length的数据报,并指定发送目的IP与端口号 |
byte[] | getData() | 从数据缓冲区中读取数据 |
int | getLength() | 返回发送或接收的数据长度 |
2.2 UDP编程代码
2.2.1 实现需求
作为我们的第一个UDP实验,我们希望实现一个回显服务器的效果(这相当于网络编程的"Hello World"),需求如下:
- 程序分为两部分,服务器端和客户端
- 客户端可以接收键盘输入内容,封装报文向指定服务器发送数据报
- 服务器端接收数据报后在显示器上打印格式为
[/127.0.0.1, 52523]服务器接收到请求: xxx
,并回复给客户端OK
- 客户端发送数据报后等待服务器响应内容,然后将响应内容打印在显示器上
- 要求服务器可以持续接收客户端请求,客户端可以不停接收用户键盘输入
2.2.2 代码编写
UDP服务器端代码:
/*** UDP服务器端代码*/
public class UdpServer {private int serverPort = 0; // 服务器端端口private DatagramSocket socket = null;public UdpServer(int port) throws SocketException {this.serverPort = port;this.socket = new DatagramSocket(port);}public void start() throws IOException {System.out.println("服务器开始启动....");// 1. 循环处理客户端请求while (true) {// 2. 阻塞等待客户端请求DatagramPacket request = new DatagramPacket(new byte[4096], 4096);socket.receive(request);// 3. 获得请求后进行处理String responseMsg = process(request);// 4. 将响应回传客户端DatagramPacket response = new DatagramPacket(responseMsg.getBytes(), responseMsg.getBytes().length, request.getSocketAddress());socket.send(response);}}public String process(DatagramPacket request) {// 根据请求数据读取构造字符串String msg = new String(request.getData(), 0, request.getLength());System.out.printf("[%s, %d]服务器接收到请求: %s\n", request.getAddress(), request.getPort(), msg);// 服务器端返回OKreturn "OK";}public static void main(String[] args) throws IOException {UdpServer udpServer = new UdpServer(9090);udpServer.start();}
}
UDP客户端代码:
/*** UDP客户端代码*/
public class UdpClient {private String serverIP;private int serverPort;private DatagramSocket socket;public UdpClient(String serverIP, int serverPort) throws SocketException {this.serverIP = serverIP;this.serverPort = serverPort;this.socket = new DatagramSocket();}public void start() throws IOException {System.out.println("客户端启动....");Scanner scanner = new Scanner(System.in);// 1. 用户持续输入System.out.print("->");while (scanner.hasNext()) {String input = scanner.next();// 2. 将用户输入内容构造成数据报DatagramPacket request = new DatagramPacket(input.getBytes(), input.getBytes().length, InetAddress.getByName(serverIP), serverPort);// 3. 向服务器端发送数据报socket.send(request);// 4. 阻塞等待服务器端响应DatagramPacket response = new DatagramPacket(new byte[4096], 4096);socket.receive(response);// 5. 打印响应内容String responseMsg = new String(response.getData(), 0, response.getLength());System.out.println(responseMsg);System.out.print("->");}}public static void main(String[] args) throws IOException {UdpClient udpClient = new UdpClient("127.0.0.1", 9090);udpClient.start();}
}
运行效果:
客户端:
服务器端:
2.2.3 流程分析
我们以客户端输入"hello"为例分析客户端和服务器端各自的执行流程
- 服务器端执行
socket.receive(request);
进入阻塞状态,等待客户端的请求 - 客户端执行
while(scanner.hasNext()) {...}
阻塞等待用户键盘输入 - 客户端用户在键盘敲下"hello",客户端停止阻塞,执行以下代码
String input = scanner.next();
// 2. 将用户输入内容构造成数据报
DatagramPacket request = new DatagramPacket(input.getBytes(), input.getBytes().length, InetAddress.getByName(serverIP), serverPort);
// 3. 向服务器端发送数据报
socket.send(request);
// 4. 阻塞等待服务器端响应
DatagramPacket response = new DatagramPacket(new byte[4096], 4096);
socket.receive(response);
将用户输入内容构造成DatagramPacket
对象,然后执行socket.send(request)
向服务器发送请求。然后执行socket.receive(response);
进入阻塞状态,等待服务器响应
- 服务器端停止阻塞,开始执行以下代码
// 3. 获得请求后进行处理
String responseMsg = process(request);
// 4. 将响应回传客户端
DatagramPacket response = new DatagramPacket(responseMsg.getBytes(), responseMsg.getBytes().length, request.getSocketAddress());
socket.send(response);
服务器获得请求数据报后开始解析,然后构建响应数据报返回给客户端,即调用socket.send(response);
,向客户端发送数据报socket.send(response);
,之后再次执行while循环执行socket.receive(request);
,阻塞等待下一次的客户端请求
- 客户端接收到服务器端响应,停止阻塞,执行以下代码
// 5. 打印响应内容
String responseMsg = new String(response.getData(), 0, response.getLength());
System.out.println(responseMsg);
System.out.print("->");
将响应内容显示在屏幕上后,继续执行while(scanner.hasNext()) {...}
进入阻塞等待下一次用户输入,由此进入闭环。
总结:无论是客户端还是服务器端,都需要各自执行通过套接字发送请求、接收响应的过程即客户端调用一次send、一次receive方法,服务器端调用一次send、一次receive方法。而且send方法中的参数一定是载有实际发送内容的字节数组,而receive方法参数所需的DatagramPacket对象内部则为空的字节数据,是需要被响应内容所填充的 输出型参数。
完整流程图: