【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);
// 服务器端返回OK
return "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对象内部则为空的字节数据,是需要被响应内容所填充的 输出型参数。
完整流程图: