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 套接字
DatagramSocket datagramSocket = new DatagramSocket();
  • 创建时不需要指定本地的地址和端口号。
UDP 套接字的重要方法
  1. 发送网络数据

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

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

    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. 发送信息的构造方法

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

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

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

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

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

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

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

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

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

    • 使用套接字的sendreceive方法来发送和接收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); 
}
工作流程
  1. 创建缓冲区:在服务器启动时,创建一个字节数组作为数据缓存区,以存放接收到的UDP数据报文。
  2. 进入处理循环:服务器进入无限循环,等待客户端的请求。
  3. 接收数据:当客户端请求到达时,通过serverSocket.receive(inPacket)方法阻塞等待,直到有数据到达。
  4. 处理请求:从inPacket中读取客户端发送的数据,处理相应的业务逻辑。
  5. 发送响应:
    • 根据处理结果创建响应数据,并将其封装到新的DatagramPacket中。
    • 使用serverSocket.send(outPacket)将响应发送回客户端。
  6. 重置长度:在每次接收数据之前,调用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
上一篇:【算法】(Python)贪心算法


下一篇:C语言 | Leetcode C语言题解之第538题把二叉搜索树转换为累加树-题解: