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();Platform.runLater(() -> OutputArea.appendText("连接服务器失败: " + e.getMessage() + "\n"));}});btnExit.setOnAction(event -> {//TODO 退出程序System.exit(0);});btnSend.setOnAction(event -> {//TODO 发送消息String message = InputField.getText().trim();if (message.isEmpty()) {return;}client.send(message);InputField.clear();});// 添加滚轮事件OutputArea.setOnScroll(event -> { // event滚轮事件,从底层的gestureEvent中继承,里面定义了controlDown变量,表示是否按下了ctrl键if (event.isControlDown()) {if (event.getDeltaY() > 0) {OutputArea.setStyle("-fx-font-size: " + (OutputArea.getFont().getSize() + 1) + "px;");} else {OutputArea.setStyle("-fx-font-size: " + (OutputArea.getFont().getSize() - 1) + "px;");}}});OutputArea.setWrapText(true);primaryStage.setScene(scene);primaryStage.show();}public static void main(String[] args) {launch(args);}
}
拓展练习一: 组播程序设计
-
组播是指在一群用户范围内发送和接收信息,该信息具有共享性。UDP具有组播功能,而TCP不具有。
-
组播地址范围为
224.0.0.0 --- 239.255.255.255
。组播地址号唯一标示一群用户(一定网络范围内,仅限于局域网络内或有些自治系统内支持组播)。但有很多组播地址默认已经被占用,建议在225.0.0.0到238.255.255.255之间随机选择一个组播地址使用。(默认组播只能同一网段,不能跨子网,除非设置了TTL值,且有配置组播路由器。另外同一个子网内如果也出现部分主机组播无效,可能是vmware的虚拟网卡影响,可先临时禁用这些命名为Vmnet*的虚拟网卡,并关闭防火墙) -
只要大家加入同一个组播地址,就能全体收取信息。在Java中,使用组播套接字
MulticastSocket
来组播数据,其是DatagramSocket 的一个子类,使用方式也与DatagramSocket 十分相似:将数据放在DatagramPacket对象中,然后通过MulticastSocket
收发DatagramPacket
对象。
组播套接字类MulticastSocket及其几个重要的方法:
路由器、交换机一般只转发和终端机一致IP地址和广播地址数据,终端机如何知道要接收组内信息?
-
要先声明加入或退出某一组播组,其方法是:
MulticastSocket ms = new MulticastSocket(8900); ms.joinGroup(groupIP);
该方法表示加入groupIP 组,groupIP 是 InetAddress 类型的组播地址。
其作用是:告知自己的网络层该IP地址的包要收;转告上联的路由器这样的IP地址包要转发。
ms.leaveGroup(groupIP);
该方法表示退出 groupIP 组
-
组内接收和发送信息的方法同UDP单播,也是以下两个方法:
ms.send(DatagramPacket packet); ms.receive(DatagramPacket packet);
-
独立完成组播程序
Multicast.java
(供参考的源代码见附录)和窗体界面MulticastFX.java
,组播套接字为225.0.0.1:8900,在组内发言要求以 "From IP 地址 学号 姓名:"为信息头。 -
其效果如图6.3所示,要求每位同学都能看到组内其他同学的留言。
Multicast.java
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.InetAddress;
import java.net.MulticastSocket;
import java.nio.charset.StandardCharsets;
public class Multicast {InetAddress groupIP;int port = 8900;MulticastSocket ms = null;byte[] inBuff = new byte[1024]; // 1MB数据byte[] outBuff = new byte[1024];public Multicast() throws IOException {groupIP = InetAddress.getByName("225.0.0.1");// 开启一个组播端口ms = new MulticastSocket(port);// 告诉网卡这样的 IP 地址数据包要接收ms.joinGroup(groupIP);}public void send(String msg) {try {outBuff = ("From/" + InetAddress.getLocalHost().toString() + " " + "20221003xxx xx" + msg).getBytes(StandardCharsets.UTF_8);DatagramPacket outPacket = new DatagramPacket(outBuff, outBuff.length, groupIP, port);ms.send(outPacket);} catch (Exception e) {e.printStackTrace();}}public String receive() {try {DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);ms.receive(inPacket);String msg = new String(inPacket.getData(), 0, inPacket.getLength(), StandardCharsets.UTF_8);return "From " + inPacket.getAddress().getHostAddress() + " " + msg + "\n";} catch (Exception e) {e.printStackTrace();return null;}}public void close() {try {ms.leaveGroup(groupIP);ms.close();} catch (Exception e) {e.printStackTrace();}}
}
MulticastFx.java
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;
import java.net.InetAddress;
import java.net.UnknownHostException;public class MulticastFx extends Application {private Multicast multicast;private final Button btnExit = new Button("退出");private final Button btnSend = new Button("发送");private final TextArea OutputArea = new TextArea();private final TextField InputField = new TextField();public void start(Stage primaryStage) throws IOException {BorderPane mainPane = new BorderPane();VBox mainVBox = new VBox();//内容显示区域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(vBox, hBox2);mainPane.setCenter(mainVBox);VBox.setVgrow(vBox, Priority.ALWAYS);Scene scene = new Scene(mainPane, 800, 600);multicast = new Multicast();Thread receiveThread = new Thread(() -> {while (true) {try {String msg = multicast.receive();Platform.runLater(() -> {OutputArea.appendText(msg + "\n");});} catch (Exception e) {e.printStackTrace();}}}, "receiveThread");receiveThread.start();btnExit.setOnAction(event -> {//TODO 退出程序System.exit(0);});btnSend.setOnAction(event -> {//TODO 发送消息String message = InputField.getText().trim();if (message.isEmpty()) {return;}multicast.send(message);try {OutputArea.appendText("From/" + InetAddress.getLocalHost().toString() + " " + "20221003xxx xx" + message + "\n");} catch (UnknownHostException e) {e.printStackTrace();}InputField.clear();});// 添加滚轮事件OutputArea.setOnScroll(event -> { // event滚轮事件,从底层的gestureEvent中继承,里面定义了controlDown变量,表示是否按下了ctrl键if (event.isControlDown()) {if (event.getDeltaY() > 0) {OutputArea.setStyle("-fx-font-size: " + (OutputArea.getFont().getSize() + 1) + "px;");} else {OutputArea.setStyle("-fx-font-size: " + (OutputArea.getFont().getSize() - 1) + "px;");}}});OutputArea.setWrapText(true);primaryStage.setScene(scene);primaryStage.show();}public static void main(String[] args) {launch(args);}
}
扩展练习二:UDP局域网聊天程序
-
与TCP不同,UDP其实不真正区分服务端和客户端,一个程序其实可以身兼二职,尝试写一个不区分服务端和客户端UDP局域网聊天程序
UDPChatFX.java
-
为了能够彼此通信,使用一个约定的固定端口号,例如9527,界面可参考图 6.4。在同一局域网网段的机器运行该程序,可以互相
发消息及发送广播消息。 程序应该提供一个下拉组合框来显示在线的用户IP地址,选中地址即可以给该用户发送消息;如果下拉组
合框的内容为空,则给所有用户发送广播消息。发送广播消息可以简单的给广播地址"255.255.255.255"发送报文来实现。
- 下拉组合框可以使用泛型方式的
private ComboBox ipComboBox = new ComboBox<>()
。ipComboBox.setEditable(true)
将组合框设置成可编辑,ipComboBox.getValue()
可获取组合框中选定的内容,ipComboBox.getItems().add(ipString)
可以添加 IP 地址到组合框,ipComboBox.getItems().clear()
可以清空组合框,具体其他用法可以自行搜索查询。 - 关于在线IP地址列表的获得方法,可以给广播地址发送一个约定的探测信息,收到该特定探测信息的用户就回发一个约定的信息报文,这样就可以从该报文中取出IP地址,加入到下拉组合框中。例如可以约定:点击"刷新在线用户"按钮时,向广播地址"255.255.255.255"发送特定的字符串"detect",而收到"detect"信息时,回发"echo"。通过这种统一的约定就可以找到在线用户。
UDPChat.java
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;
import java.util.HashSet;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;public class UDPChat {private final int port = 8118;private DatagramSocket socket;public InetAddress broadcastAddress; // 广播地址private Thread refreshThread; // 接收线程byte[] inBuff = new byte[512]; // 512字节 = 512Bbyte[] outBuff = new byte[512];// 创建一个数组private final HashSet<String> onlineUsers = new HashSet<>();public UDPChat() {try {socket = new DatagramSocket(port);socket.setBroadcast(true);broadcastAddress = InetAddress.getByName("255.255.255.255");// startRefreshThread(onlineUsers);} catch (Exception e) {e.printStackTrace();}}public void send(String msg, int type, InetAddress address) {try {if (type == 1) { // 群播outBuff = ("From/" + InetAddress.getLocalHost().toString() + " " + "20221003xxx xx " + msg).getBytes(StandardCharsets.UTF_8);DatagramPacket outPacket = new DatagramPacket(outBuff, outBuff.length, broadcastAddress, port);socket.send(outPacket);} else if (type == 2) { // 单播System.out.println("单播消息:" + msg);outBuff = ("单播消息:" + msg).getBytes(StandardCharsets.UTF_8);DatagramPacket outPacket = new DatagramPacket(outBuff, outBuff.length, address, port);socket.send(outPacket);} else if (type == 3) { // 刷新`在线用户`outBuff = (msg).getBytes(StandardCharsets.UTF_8);DatagramPacket outPacket = new DatagramPacket(outBuff, outBuff.length, InetAddress.getByName("255.255.255.255"), port);socket.send(outPacket);}} catch (Exception e) {e.printStackTrace();}}public String receive() {try {DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);socket.receive(inPacket);String msg = new String(inPacket.getData(), 0, inPacket.getLength(), StandardCharsets.UTF_8);if (msg.equals("detect")) {// 发送检测请求send("echo", 3, null);return "detect";}return "receive: " + msg;} catch (Exception e) {e.printStackTrace();return null;}}public boolean isClosed() {return socket.isClosed();}public void close() {// 关闭套接字socket.close();System.out.println("Socket closed.");}// public void startRefreshThread(HashSet<String> onlineUsers) {// // 接收刷新在线用户响应// this.refreshThread = new Thread(() -> {// while (true) {// try {// DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);// socket.receive(inPacket);// String msg = new String(inPacket.getData(), 0, inPacket.getLength(), StandardCharsets.UTF_8);// if (msg.equals("echo")) {// String usrAddr = inPacket.getAddress().toString().substring(1);// onlineUsers.add(usrAddr);// }// } catch (Exception e) {// e.printStackTrace();// } finally {// // 加入适当的休眠时间// try {// Thread.sleep(5000); // 100毫秒// } catch (InterruptedException e) {// Thread.currentThread().interrupt(); // 恢复中断状态// }// }// }// }, "RefreshThread");// refreshThread.start();// }public void RefreshUsers( HashSet<String> onlineUsers ) {long startTime = System.currentTimeMillis();long endTime = startTime + 5000; // 设置结束时间为当前时间加上5秒ExecutorService executor = Executors.newSingleThreadExecutor();Future<?> future = executor.submit(() -> {while (System.currentTimeMillis() < endTime) {try {DatagramPacket inPacket = new DatagramPacket(inBuff, inBuff.length);socket.receive(inPacket);String msg = new String(inPacket.getData(), 0, inPacket.getLength(), StandardCharsets.UTF_8);if ("echo".equals(msg)) {String usrAddr = inPacket.getAddress().toString().substring(1);onlineUsers.add(usrAddr);}} catch (Exception e) {e.printStackTrace();}}});try {// 等待任务完成或者超时future.get(5, java.util.concurrent.TimeUnit.SECONDS);} catch (Exception e) {e.printStackTrace();} finally {executor.shutdownNow(); // 尝试立即停止所有正在执行的任务try {if (!executor.awaitTermination(5, java.util.concurrent.TimeUnit.SECONDS)) {executor.shutdownNow(); // 再次尝试强制停止}} catch (InterruptedException ex) {executor.shutdownNow();Thread.currentThread().interrupt(); // 恢复中断状态}}}public HashSet<String> refreshOnlineUsers() {// 发送刷新在线用户请求send("detect", 3, null);// 刷新在线用户RefreshUsers(onlineUsers);return onlineUsers;}
}
UDPChatFx.java
import javafx.application.Application;
import javafx.application.Platform;
import javafx.geometry.Insets;
import javafx.geometry.Pos;
import javafx.scene.Scene;
import javafx.scene.control.*;
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.net.InetAddress;
import java.net.UnknownHostException;
import java.util.HashSet;import static javafx.scene.input.KeyCode.ENTER;public class UDPChatFx extends Application {private final UDPChat chat = new UDPChat();private final TextArea Output = new TextArea();private final TextField Input = new TextField();private final Button refreshButton = new Button("刷新在线用户");private final Button sendButton = new Button("发送");private final Button closeButton = new Button("关闭");private final ComboBox<String> ipComboBox = new ComboBox<>();public void start(Stage primaryStage) {BorderPane mainPane = new BorderPane();mainPane.setPadding(new Insets(10));ipComboBox.resize(150, 20);ipComboBox.setEditable(true);ipComboBox.getItems().add("所有用户");ipComboBox.getSelectionModel().select("所有用户");// 设置对话框区域VBox vbox = new VBox(10);vbox.getChildren().addAll(new Label("对话框"), Output);VBox.setVgrow(Output, Priority.ALWAYS);Output.setEditable(false);// 设置输入区域HBox hbox = new HBox(10);hbox.setAlignment(Pos.CENTER);HBox.setHgrow(Input, Priority.ALWAYS);hbox.getChildren().addAll(ipComboBox, refreshButton, Input, sendButton, closeButton);VBox mainVBox = new VBox(10);mainVBox.getChildren().addAll(vbox, hbox);mainPane.setCenter(mainVBox);VBox.setVgrow(vbox, Priority.ALWAYS);Thread ReceiveThread = new Thread(() -> {// 退出线程while (!chat.isClosed()) {String msg = chat.receive();System.out.println("接收到消息: " + msg);Platform.runLater(() -> Output.appendText(msg + "\n"));}}, "ReceiveThread");ReceiveThread.start();// 设置关闭按钮事件closeButton.setOnAction(e -> {chat.close();System.exit(0);});// 设置刷新按钮事件refreshButton.setOnAction(e -> {System.out.println("刷新在线用户列表");HashSet<String> onlineUsers = chat.refreshOnlineUsers();ipComboBox.getItems().clear();ipComboBox.getItems().add("所有用户");ipComboBox.getItems().addAll(onlineUsers);ipComboBox.getSelectionModel().select("所有用户");});Input.setOnKeyPressed(e -> {if (e.getCode() == ENTER) {sendButton.fire();}});// 设置发送按钮事件sendButton.setOnAction(e -> {String msg = Input.getText();if (msg.isEmpty()) {return;}if (ipComboBox.getSelectionModel().getSelectedItem().equals("所有用户")) {System.out.println("群发: " + msg);chat.send(msg, 1, null); // 默认群发} else {InetAddress ip = null;try {ip = InetAddress.getByName(ipComboBox.getSelectionModel().getSelectedItem());} catch (UnknownHostException ex) {throw new RuntimeException(ex);}chat.send(msg, 2, ip); // 指定用户群发}Output.appendText("我: " + msg + "\n");Input.clear();});primaryStage.setScene(new Scene(mainPane, 760, 450));primaryStage.setTitle("UDP Chat Application");primaryStage.show();}public static void main(String[] args) {launch(args);}
}