chapter06(UDPSocket)
UPD的特点
- UDP有独立的套接字(IP + PORT),与TCP使用相同端口号不会冲突。
- UDP在使用前不需要进行连接,没有流的概念。
- UDP通信类似于邮件通信:不需要实时连接,只需要目的地址。
- UDP通信前只需知道对方的IP地址和端口号即可发送信息。
- 基于用户数据报文(包)进行读写。
- UDP通信通常用于线路质量好的环境,如局域网。如果在互联网上,通常用于对数据完整性要求不高的场合,例如语音传送等。
UDP 编程关键Java类
DatagramSocket
DatagramPacket
MulticastSocket
1.创建 UDPClient.java 程序
UDP 客户端的主要步骤
-
创建 DatagramSocket 实例
- 可以选择对本地地址和端口号进行设置,但一般不需要指定。
- 不指定时程序将自动选择本地地址和可用的端口。
-
发送和接收数据
- 使用
DatagramSocket
类来发送和接收DatagramPacket
类的实例进行通信。
- 使用
-
关闭套接字
- 通信完成后,使用
DatagramSocket
类的close()
方法销毁该套接字。
- 通信完成后,使用
注意事项
- 与
Socket
类不同,创建DatagramSocket
实例时并不需要指定目的地址,这也是 TCP 协议和 UDP 协议的最大不同点之一。
UDP 套接字类: DatagramSocket
概述
- UDP通信没有客户套接字 (
Socket
用于通信) 和服务器套接字 (ServerSocket
服务器端用于接收连接请求) 之分,UDP套接字只有一种:DatagramSocket
。 - UDP套接字的角色类似于邮箱,可以从不同地址接收邮件,并向不同地址发送信息。
- UDP编程不严格区分服务端和客户端,通常将固定IP和固定端口的机器视为服务器。
创建 UDP 套接字
DatagramSocket datagramSocket = new DatagramSocket();
- 创建时不需要指定本地的地址和端口号。
UDP 套接字的重要方法
-
发送网络数据
datagramSocket.send(DatagramPacket packet);
- 发送一个数据包到由IP和端口号指定的地址。
-
接收网络数据
datagramSocket.receive(DatagramPacket packet);
- 接收一个数据包。如果没有数据,程序会在此调用处阻塞。
-
指定超时
datagramSocket.setSoTimeout(int timeout);
-
timeout
是一个整数,表示毫秒数,用于指定receive(DatagramPacket packet)
方法的最长阻塞时间。 - 超过此时限后,如果没有响应,将抛出
InterruptedIOException
异常。
-
注意事项
- 如果客户端通过
send
发送信息并等待响应,则可以设置超时,避免程序无限等待。 - 如果采用类似TCP的设计,开启新线程接收信息,则不应使用超时设置,以避免在等待过程中导致超时错误。
UDP 数据报文类: DatagramPacket
概述
- TCP发送数据是基于字节流的,而UDP发送数据是基于
DatagramPacket
报文。 - 网络中传递的UDP数据都封装在自包含(self-contained)的报文中。
发送数据的过程
- 在
创建UDP套接字时
,没有指定远程通信方的IP和端口,而send
方法的参数(DatagramPacket packet)
是关键。 - 每个数据报文实例除了包含要传输的信息外,还附加了IP地址和端口信息,这些信息的含义取决于数据报文是被发送还是被接收。
数据报文的创建
-
发送信息的构造方法
DatagramPacket(byte[] data, int length, InetAddress remoteAddr, int remotePort);
- 需要明确远程地址信息,以便将报文发送到目的地址。
-
接收信息的构造方法
DatagramPacket(byte[] data, int length);
- 不需要指定地址信息,
length
表示要读取的数据长度,data
是用于存储报文数据的字节数组缓存。
- 不需要指定地址信息,
UDP 数据报文的几个重要方法
-
获取目标主机IP地址
InetAddress getAddress();
- 如果是发送的报文,返回目标主机的IP地址;如果是接收的报文,返回发送该数据报文的主机IP地址。
-
获取目标主机端口
int getPort();
- 如果是发送的报文,返回目标主机的端口;如果是接收的报文,返回发送该数据报文的主机端口。
-
获取与报文相关联的数据
byte[] getData();
- 从报文中取出数据,返回与数据报文相关联的字节数组。
注意事项
- 上述两个方法 (
getAddress()
和getPort()
) 主要供服务端使用,服务端可以通过这些方法获知客户端的地址信息。
2.创建UDPClientFX.java客户端窗体程序
创建 UDPServer.java 程序
概述
- 类似TCP服务器,UDP服务器的工作是建立一个通信终端,并被动等待客户端发起连接。
- 由于UDP是无连接的,因此没有TCP中建立连接的步骤。
- UDP通信通过客户端的数据报文进行初始化。
典型的UDP服务器步骤
-
创建UDP套接字
- 创建一个
DatagramSocket
实例,并指定一个本地端口(端口号范围在1024-65535之间选择)。
DatagramSocket datagramSocket = new DatagramSocket(port);
- 创建一个
- 服务器准备好从任何客户端接收数据报文。
- UDP服务器为所有客户端使用同一个套接字(与TCP不同,TCP服务器为每个成功的
accept
方法调用创建新的套接字)。
-
接收UDP报文
- 使用
DatagramSocket
实例的receive
方法接收一个DatagramPacket
实例。
datagramSocket.receive(datagramPacket);
- 当
receive
方法返回时,数据报文将包含客户端的地址信息,从而使服务器知道该消息的来源,以便进行回复。
- 使用
-
通信过程
- 使用套接字的
send
和receive
方法来发送和接收DatagramPacket
的实例进行通信。
- 使用套接字的
注意事项
-
服务端需要循环调用
receive
方法接收消息。 -
如果使用同一个报文实例来接收消息,在下一个
receive
方法调用之前,需要调用报文实例的setLength(缓存数组.length)
方法,以确保兼容性,避免数据丢失的BUG。datagramPacket.setLength(缓存数组.length);
-
每次
receive
接收到的报文会修改内部消息的长度值。如果接收到的消息是10字节,下一次receive
接收超出10字节的内容将会被丢弃。因此,务必重置长度值以防数据丢失。
UDP 服务器处理方法
注意事项
- 与TCP不同,小负荷的UDP服务器通常
不采用多线程方式
。 - 由于UDP使用同一个套接字对应多个客户端,UDP服务器可以简单地使用顺序迭代的方式处理请求,而无需创建多个线程。
处理模式
- UDP服务器的工作模式可以直接按照以下步骤进行:
// 省略......
byte[] buffer = new byte[MAX_PACKET_SIZE]; // 创建数据缓存区
DatagramPacket inPacket = new DatagramPacket(buffer, buffer.length); // 创建接收数据报文
// 省略.....
while (true) {
// 等待客户端请求
serverSocket.receive(inPacket); // 阻塞等待,来了哪个客户端就服务哪个客户端
// 处理请求
String receivedData = new String(inPacket.getData(), 0, inPacket.getLength()); // 读取客户端发送的数据
System.out.println("收到来自客户端的消息: " + receivedData);
// 发送响应数据
String response = "服务器已收到: " + receivedData;
byte[] responseData = response.getBytes();
DatagramPacket outPacket = new DatagramPacket(responseData, responseData.length, inPacket.getAddress(), inPacket.getPort());
serverSocket.send(outPacket); // 发送响应给客户端
// 每次调用前,重置报文内部消息长度为缓冲区的实际长度
inPacket.setLength(buffer.length);
}
工作流程
- 创建缓冲区:在服务器启动时,创建一个字节数组作为数据缓存区,以存放接收到的UDP数据报文。
- 进入处理循环:服务器进入无限循环,等待客户端的请求。
-
接收数据:当客户端请求到达时,通过
serverSocket.receive(inPacket)
方法阻塞等待,直到有数据到达。 -
处理请求:从
inPacket
中读取客户端发送的数据,处理相应的业务逻辑。 - 发送响应:
- 根据处理结果创建响应数据,并将其封装到新的
DatagramPacket
中。 - 使用
serverSocket.send(outPacket)
将响应发送回客户端。
- 根据处理结果创建响应数据,并将其封装到新的
-
重置长度:在每次接收数据之前,调用
inPacket.setLength(buffer.length)
,以确保能够正确接收下一次数据,避免出现数据丢失。
优势
- 这种单线程顺序处理方法简单易懂,适用于负载较轻的场景,可以有效减少服务器资源的占用。
- 与多线程相比,能避免上下文切换和线程管理带来的额外开销。
预习版本代码
UDPServer
package server;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.util.Date;
public class UDPServer {
private final int port = 8888;
private DatagramSocket socket;
public UDPServer() {
try {
socket = new DatagramSocket(port);
System.out.println("Server started on port " + port);
} catch (Exception e) {
e.printStackTrace();
}
}
public void Service(){
while (true) {
byte[] buffer = new byte[1024];
DatagramPacket packet = new DatagramPacket(buffer, buffer.length);
try {
socket.receive(packet);
String message = new String(packet.getData(), 0, packet.getLength());
System.out.println("Received message: " + message);
String response = "20221003174&徐彬&"+ new Date() + "&" + message;
byte[] responseBytes = response.getBytes();
// 返回响应
DatagramPacket responsePacket = new DatagramPacket(responseBytes, responseBytes.length, packet.getAddress(), packet.getPort());
socket.send(responsePacket);
} catch (Exception e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
new UDPServer().Service();
}
}
UDPClient
package client;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
public class UDPClient {
private final int remotePort;
private final InetAddress remoteIP;
private final DatagramSocket socket; // UDP套接字
//用于接收数据的报文字节数组缓存最大容量,字节为单位
private static final int MAX_PACKET_SIZE = 512;
// private static final int MAX_PACKET_SIZE = 65507;
public UDPClient(String remoteIP, String remotePort) throws IOException {
this.remoteIP = InetAddress.getByName(remoteIP);
this.remotePort = Integer.parseInt(remotePort);
// 创建UDP套接字,系统随机选定一个未使用的UDP端口绑定
socket = new DatagramSocket(); // 其实就是创建了一个发送datagram包的socket
//设置接收数据超时
// socket.setSoTimeout(30000);
}
public void send(String msg) {
try {
//将待发送的字符串转为字节数组
byte[] outData = msg.getBytes(StandardCharsets.UTF_8);
//构建用于发送的数据报文,构造方法中传入远程通信方(服务器)的ip地址和端口
DatagramPacket outPacket = new DatagramPacket(outData, outData.length, remoteIP, remotePort);
// 给UDPServer发送数据报文
socket.send(outPacket);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
// 定义数据接收方法
public String receive() {
String msg = null;
// 先准备一个空数据报文
DatagramPacket inPacket = new DatagramPacket(new byte[MAX_PACKET_SIZE], MAX_PACKET_SIZE);
try {
//读取报文,阻塞语句,有数据就装包在inPacket报文中,装完或装满为止。
socket.receive(inPacket);
//将接收到的字节数组转为字符串
msg = new String(inPacket.getData(), 0, inPacket.getLength(), StandardCharsets.UTF_8);
} catch (IOException e) {
e.printStackTrace();
}
return msg;
}
}
UDPClientFx
package client;
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.Button;
import javafx.scene.control.Label;
import javafx.scene.control.TextArea;
import javafx.scene.control.TextField;
import javafx.scene.input.KeyCode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.HBox;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.Stage;
import java.io.IOException;
public class UDPClientFx extends Application {
private UDPClient client;
private final Button btnInit = new Button("初始");
private final Button btnExit = new Button("退出");
private final Button btnSend = new Button("发送");
private final TextField IpAdd_input = new TextField();
private final TextField Port_input = new TextField();
private final TextArea OutputArea = new TextArea();
private final TextField InputField = new TextField();
public void start(Stage primaryStage) {
BorderPane mainPane = new BorderPane();
VBox mainVBox = new VBox();
HBox hBox = new HBox();
hBox.setSpacing(10);//各控件之间的间隔
//HBox面板中的内容距离四周的留空区域
hBox.setPadding(new Insets(20, 20, 10, 20));
hBox.getChildren().addAll(new Label("IP地址: "), IpAdd_input, new Label("端口: "), Port_input, btnInit);
hBox.setAlignment(Pos.TOP_CENTER);
//内容显示区域
VBox vBox = new VBox();
vBox.setSpacing(10);//各控件之间的间隔
//VBox面板中的内容距离四周的留空区域
vBox.setPadding(new Insets(10, 20, 10, 20));
vBox.getChildren().addAll(new Label("信息显示区:"), OutputArea, new Label("信息输入区"), InputField);
//设置显示信息区的文本区域可以纵向自动扩充范围
VBox.setVgrow(OutputArea, Priority.ALWAYS);
// 设置文本只读和自动换行
OutputArea.setEditable(false);
OutputArea.setStyle("-fx-wrap-text: true; /* 实际上是默认的 */ -fx-font-size: 18px;");
InputField.setOnKeyPressed(event -> {
if (event.getCode() == KeyCode.ENTER) {
btnSend.fire();
}
});
//底部按钮区域
HBox hBox2 = new HBox();
hBox2.setSpacing(10);
hBox2.setPadding(new Insets(10, 20, 10, 20));
hBox2.setAlignment(Pos.CENTER_RIGHT);
hBox2.getChildren().addAll(btnSend, btnExit);
mainVBox.getChildren().addAll(hBox, vBox, hBox2);
mainPane.setCenter(mainVBox);
VBox.setVgrow(vBox, Priority.ALWAYS);
Scene scene = new Scene(mainPane, 800, 600);
IpAdd_input.setText("127.0.0.1");
Port_input.setText("8888");
btnInit.setOnAction(event -> {
try {
String ip = IpAdd_input.getText().trim();
String port = Port_input.getText().trim();
client = new UDPClient(ip, port);
client.send("Hello, Server!");
new Thread(() -> {
while (true) {
String message = client.receive();
if (message != null && !message.isEmpty()) {
Platform.runLater(() -> OutputArea.appendText(message + "\n"));
}
}
}).start(); // 启动接收线程
} catch (IOException e) {
e.printStackTrace