JavaFx -- chapter06(UDPSocket)

chapter06(UDPSocket)

UPD的特点

  1. UDP有独立的套接字(IP + PORT),与TCP使用相同端口号不会冲突。
  2. UDP在使用前不需要进行连接,没有流的概念。
  3. UDP通信类似于邮件通信:不需要实时连接,只需要目的地址。
  4. UDP通信前只需知道对方的IP地址和端口号即可发送信息。
  5. 基于用户数据报文(包)进行读写。
  6. UDP通信通常用于线路质量好的环境,如局域网。如果在互联网上,通常用于对数据完整性要求不高的场合,例如语音传送等。

UDP 编程关键Java类

  • DatagramSocket
  • DatagramPacket
  • MulticastSocket

1.创建 UDPClient.java 程序

UDP 客户端的主要步骤
  1. 创建 DatagramSocket 实例

    • 可以选择对本地地址和端口号进行设置,但一般不需要指定。
    • 不指定时程序将自动选择本地地址和可用的端口。
  2. 发送和接收数据

    • 使用 DatagramSocket 类来发送和接收 DatagramPacket 类的实例进行通信。
  3. 关闭套接字

    • 通信完成后,使用 DatagramSocket 类的 close() 方法销毁该套接字。
注意事项
  • Socket 类不同,创建 DatagramSocket 实例时并不需要指定目的地址,这也是 TCP 协议和 UDP 协议的最大不同点之一。

UDP 套接字类: DatagramSocket

概述
  • UDP通信没有客户套接字 (Socket用于通信) 和服务器套接字 (ServerSocket服务器端用于接收连接请求) 之分,UDP套接字只有一种:DatagramSocket
  • UDP套接字的角色类似于邮箱,可以从不同地址接收邮件,并向不同地址发送信息。
  • UDP编程不严格区分服务端和客户端,通常将固定IP和固定端口的机器视为服务器。
创建 UDP 套接字
java 复制代码
DatagramSocket datagramSocket = new DatagramSocket();
  • 创建时不需要指定本地的地址和端口号。
UDP 套接字的重要方法
  1. 发送网络数据

    java 复制代码
    datagramSocket.send(DatagramPacket packet);
    • 发送一个数据包到由IP和端口号指定的地址。
  2. 接收网络数据

    java 复制代码
    datagramSocket.receive(DatagramPacket packet);
    • 接收一个数据包。如果没有数据,程序会在此调用处阻塞。
  3. 指定超时

    java 复制代码
    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地址和端口信息,这些信息的含义取决于数据报文是被发送还是被接收。
数据报文的创建
  1. 发送信息的构造方法

    java 复制代码
    DatagramPacket(byte[] data, int length, InetAddress remoteAddr, int remotePort);
  • 需要明确远程地址信息,以便将报文发送到目的地址。
  1. 接收信息的构造方法

    java 复制代码
    DatagramPacket(byte[] data, int length);
    • 不需要指定地址信息,length 表示要读取的数据长度,data 是用于存储报文数据的字节数组缓存。
UDP 数据报文的几个重要方法
  1. 获取目标主机IP地址

    java 复制代码
    InetAddress getAddress();
    • 如果是发送的报文,返回目标主机的IP地址;如果是接收的报文,返回发送该数据报文的主机IP地址。
  2. 获取目标主机端口

    java 复制代码
    int getPort();
    • 如果是发送的报文,返回目标主机的端口;如果是接收的报文,返回发送该数据报文的主机端口。
  3. 获取与报文相关联的数据

    java 复制代码
    byte[] getData();
    • 从报文中取出数据,返回与数据报文相关联的字节数组。
注意事项
  • 上述两个方法 (getAddress()getPort()) 主要供服务端使用,服务端可以通过这些方法获知客户端的地址信息。

2.创建UDPClientFX.java客户端窗体程序

创建 UDPServer.java 程序
概述
  • 类似TCP服务器,UDP服务器的工作是建立一个通信终端,并被动等待客户端发起连接。
  • 由于UDP是无连接的,因此没有TCP中建立连接的步骤。
  • UDP通信通过客户端的数据报文进行初始化。
典型的UDP服务器步骤
  1. 创建UDP套接字

    • 创建一个DatagramSocket实例,并指定一个本地端口(端口号范围在1024-65535之间选择)。
    java 复制代码
    DatagramSocket datagramSocket = new DatagramSocket(port);
  • 服务器准备好从任何客户端接收数据报文。
  • UDP服务器为所有客户端使用同一个套接字(与TCP不同,TCP服务器为每个成功的accept方法调用创建新的套接字)。
  1. 接收UDP报文

    • 使用DatagramSocket实例的receive方法接收一个DatagramPacket实例。
    java 复制代码
    datagramSocket.receive(datagramPacket);
    • receive方法返回时,数据报文将包含客户端的地址信息,从而使服务器知道该消息的来源,以便进行回复。
  2. 通信过程

    • 使用套接字的sendreceive方法来发送和接收DatagramPacket的实例进行通信。
注意事项
  • 服务端需要循环调用receive方法接收消息。

  • 如果使用同一个报文实例来接收消息,在下一个receive方法调用之前,需要调用报文实例的setLength(缓存数组.length)方法,以确保兼容性,避免数据丢失的BUG。

    java 复制代码
    datagramPacket.setLength(缓存数组.length);
  • 每次receive接收到的报文会修改内部消息的长度值。如果接收到的消息是10字节,下一次receive接收超出10字节的内容将会被丢弃。因此,务必重置长度值以防数据丢失。

UDP 服务器处理方法

注意事项
  • 与TCP不同,小负荷的UDP服务器通常不采用多线程方式
  • 由于UDP使用同一个套接字对应多个客户端,UDP服务器可以简单地使用顺序迭代的方式处理请求,而无需创建多个线程。
处理模式
  • UDP服务器的工作模式可以直接按照以下步骤进行:
java 复制代码
// 省略...... 
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); 
}
工作流程
  1. 创建缓冲区:在服务器启动时,创建一个字节数组作为数据缓存区,以存放接收到的UDP数据报文。
  2. 进入处理循环:服务器进入无限循环,等待客户端的请求。
  3. 接收数据 :当客户端请求到达时,通过serverSocket.receive(inPacket)方法阻塞等待,直到有数据到达。
  4. 处理请求 :从inPacket中读取客户端发送的数据,处理相应的业务逻辑。
  5. 发送响应:
    • 根据处理结果创建响应数据,并将其封装到新的DatagramPacket中。
    • 使用serverSocket.send(outPacket)将响应发送回客户端。
  6. 重置长度 :在每次接收数据之前,调用inPacket.setLength(buffer.length),以确保能够正确接收下一次数据,避免出现数据丢失。
优势
  • 这种单线程顺序处理方法简单易懂,适用于负载较轻的场景,可以有效减少服务器资源的占用。
  • 与多线程相比,能避免上下文切换和线程管理带来的额外开销。

预习版本代码

UDPServer
java 复制代码
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
java 复制代码
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
java 复制代码
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地址和广播地址数据,终端机如何知道要接收组内信息?

  • 要先声明加入或退出某一组播组,其方法是:

    java 复制代码
    MulticastSocket ms = new MulticastSocket(8900);  
    ms.joinGroup(groupIP);

    该方法表示加入groupIP 组,groupIP 是 InetAddress 类型的组播地址。

    其作用是:告知自己的网络层该IP地址的包要收;转告上联的路由器这样的IP地址包要转发。

    java 复制代码
    ms.leaveGroup(groupIP);

    该方法表示退出 groupIP 组

  • 组内接收和发送信息的方法同UDP单播,也是以下两个方法:

    java 复制代码
    ms.send(DatagramPacket packet);
    ms.receive(DatagramPacket packet); 
  • 独立完成组播程序Multicast.java(供参考的源代码见附录)和窗体界面MulticastFX.java,组播套接字为225.0.0.1:8900,在组内发言要求以 "From IP 地址 学号 姓名:"为信息头。

  • 其效果如图6.3所示,要求每位同学都能看到组内其他同学的留言。

Multicast.java
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
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
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字节 = 512B
    byte[] 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
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);
    }
}
相关推荐
艺术家天选2 分钟前
STM32点亮LED灯
stm32·单片机·嵌入式硬件
向阳逐梦3 分钟前
基于STM32F4单片机实现ROS机器人主板
stm32·单片机·机器人
WANGWUSAN6610 分钟前
Python高频写法总结!
java·linux·开发语言·数据库·经验分享·python·编程
Yvemil710 分钟前
《开启微服务之旅:Spring Boot 从入门到实践》(一)
java
forNoWhat19 分钟前
java小知识点:比较器
java·开发语言
西洼工作室25 分钟前
【java 正则表达式 笔记】
java·笔记·正则表达式
40岁的系统架构师27 分钟前
1 JVM JDK JRE之间的区别以及使用字节码的好处
java·jvm·python
皓木.27 分钟前
(自用)配置文件优先级、SpringBoot原理、Maven私服
java·spring boot·后端
舞者H30 分钟前
启动异常:Caused by: java.lang.IllegalStateException: Failed to introspect Class
java
代码中の快捷键31 分钟前
java开发面试有2年经验
java·开发语言·面试