JavaSocket 网络编程之 UDP

JavaSocket 网络编程之 UDP

1、关于 UDP 协议

1.1、什么是 UDP 协议

UDP 协议是一种面向无连接的传输层协议,其为应用程序提供了一种无需建立连接就可以发送封装的IP 数据报的方法,即面向无连接。

UDP协议会把数据打包发送给目标地址,这个数据包能不能发送给目标地址就不管了。UDP的主要特点是传输效率高,对实时性要求较高的数据传输场合比较适用。

1.2、UDP 协议的优缺点

  • 优点
    • 高效性和低延迟:UDP免除了建立和维护连接状态的开销。
    • 简洁性:数据包格式较为紧凑。
    • 支持多种通信模式:UDP支持广播和多播功能,可以将数据发送到多个接收者。
  • 缺点
    • 不可靠性:UDP不保证数据包的顺序、完整性或可靠性,这可能导致数据丢失、重复或乱序。
    • 缺乏拥塞控制:UDP没有内置的拥塞控制机制,可能在网络拥堵时导致更多的问题。
    • 安全性较低:UDP协议本身不提供加密或认证机制,易受到中间人攻击和数据篡改。可以在应用层实现加密和认证,来增强安全性。

1.3、UDP 协议的应用场景

  • ① 实时音视频通信:如VoIP和视频会议,UDP能够提供快速的数据传输,满足实时性需求。
  • ② 在线游戏:为了减少玩家操作的延迟,UDP成为了许多在线游戏的首选协议。
  • ③ DNS解析:UDP的轻量特性适合处理短小的DNS查询,提供快速的域名解析服务。
  • ④ 流媒体服务:UDP用于流媒体服务,以快速传递音视频数据,尽管不保证数据的可靠性。
  • ⑤ 网络广播:UDP支持广播功能,适用于校园广播、公司内部通知广播等场景。

1.4、UDP 与 TCP 的区别

UDP协议与TCP协议不同,UDP在传输数据前不需要建立连接,也不提供数据保证机制,如数据包的顺序、完整性或可靠性保证。UDP协议不需要类似 TCP 协议的三次握手。

HTTP(超文本传输协议)是基于TCP的。

UDP 协议 TCP 协议
连接性 无连接协议,发送数据前不需要建立连接 面向连接协议,发送数据前需要建立连接
速度和效率 传输速度快,效率高,不受拥塞控制的限制 传输速度相对较慢,因为需要建立连接和使用确认重传机制
可靠性 不保证数据包的顺序、完整性或可靠性 对数据的可靠性要求非常严格,通过确认和重传机制确保数据的完整性和正确性
数据包大小 允许将多个数据包打包成一个较大的数据报进行传输 将数据划分为较小的数据包进行传输,并根据网络状况进行调整
适用场景 实时性要求高、对丢包容忍度较高的应用,如音视频流传输、在线游戏等 对数据可靠性要求较高的应用,如文件传输、电子邮件和网页浏览等

2、编码示例

编码示例是基于SpringBoot 做的,持续监听端口的 UDP 服务。

2.1、说明

使用线程池来管理UDP监听任务可以提高资源利用率和系统的稳定性。但是对于UDP监听来说,通常只需要一个或少数几个线程来持续监听端口,因为UDP是无连接的,每个数据包都是独立的,并且监听端口本身是一个阻塞操作

然而,可以将UDP处理逻辑(即接收数据包后的处理)放在线程池中执行,以便并行处理多个数据包。那么需要将UDP 监听和数据包处理分开。监听仍然可以在一个单独的线程中完成,但一旦接收到数据包,就可以将处理任务提交给线程池。

  • com.zim.udp.UdpClient 类模拟 客户端发送消息
  • com.zim.udp.UdpListener 类 用于持续监听消息
  • com.zim.udp.UdpProcessTask 类 用于业务处理数据包(采用线程池)
  • com.zim.udp.UdpConfig 类用于配置类

2.2、代码示例

2.2.1、UdpClient

java 复制代码
package com.zim.udp;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.*;
import java.nio.charset.StandardCharsets;

/**
 * @author
 * udp 发送
 */
@Slf4j
public class UdpClient {

    public static void main(String[] args) throws IOException {
        // 1、创建发送端 socket 对象
        DatagramSocket datagramSocket = new DatagramSocket();
        // 2、提供数据,并将数据封装到数据包中
        byte[] msg = "this is a udpMessage".getBytes(StandardCharsets.UTF_8);
        // 走dns 解析获取 ip 地址 127.0.0.1
        InetAddress inetAddress = InetAddress.getByName("localhost");
        int port = 5621;
        // 参数分别为 发送数据(byte数组)、发送数据的长度、发送到服务器端的IP地址、发送服务器端的端口号
        DatagramPacket datagramPacket = new DatagramPacket(msg, msg.length, inetAddress, port);
        // 3、通过 socket 服务的发送功能,将数据包发出去
        datagramSocket.send(datagramPacket);
        log.info("{}->已发送", new String(msg));
        // 4、接收服务器响应的缓冲区
        byte[] receiveData = new byte[1024];
        DatagramPacket receivePacket = new DatagramPacket(receiveData, receiveData.length);
        // 接收服务器的响应, 这里会阻塞
        datagramSocket.receive(receivePacket);
        // 打印服务器的响应
        String sentence = new String(receivePacket.getData(), 0, receivePacket.getLength(), StandardCharsets.UTF_8);
        log.info("{}->已接收", sentence);
        // 5、释放资源
        datagramSocket.close();
    }

}

2.2.2、UdpListener

java 复制代码
package com.zim.udp;

import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.net.SocketException;
import java.util.Arrays;
import java.util.concurrent.ExecutorService;

/**
 * @author zim
 * udp 监听线程
 */
@Slf4j
public class UdpListener implements Runnable {

    private final DatagramSocket datagramSocket;
    private final ExecutorService executor;

    public UdpListener(int port, ExecutorService executor) throws SocketException {
        this.datagramSocket = new DatagramSocket(port);
        this.executor = executor;
    }

    @Override
    public void run() {
        byte[] buffer =  new byte[1024];
        DatagramPacket datagramPacket = new DatagramPacket(buffer, buffer.length);
        try {
            while (true) {
                // 这里会阻塞
                datagramSocket.receive(datagramPacket);
                // 注意:由于DatagramPacket是不可变的,实际使用中需要复制数据包内容到新的 datagramPacket 中
                InetAddress inetAddress = datagramPacket.getAddress();
                int port = datagramPacket.getPort();
                byte[] newBuffer = Arrays.copyOf(datagramPacket.getData(), datagramPacket.getLength());
                DatagramPacket newPacket = new DatagramPacket(newBuffer, newBuffer.length, inetAddress, port);
                // 提交处理任务到线程池
                executor.submit(new UdpProcessTask(newPacket));
            }
        } catch (IOException e) {
            log.error("UdpListener 接收数据失败", e);
        } finally {
            // 关闭资源
            if (datagramSocket != null) {
                datagramSocket.close();
            }
        }
    }
}

2.2.3、UdpProcessTask

java 复制代码
package com.zim.udp;

import lombok.extern.slf4j.Slf4j;

import java.net.DatagramPacket;
import java.net.DatagramSocket;
import java.net.InetAddress;
import java.nio.charset.StandardCharsets;

/**
 * @author zim
 * 处理接收到的数据包业务
 */
@Slf4j
public class UdpProcessTask implements Runnable {

    private final DatagramPacket datagramPacket;

    public UdpProcessTask(DatagramPacket datagramPacket) {
        this.datagramPacket = datagramPacket;
    }

    /**
     * 拿到数据包,处理相关业务
     */
    @Override
    public void run() {
        // 1、拿到数据包相关数据
        byte[] buffer = datagramPacket.getData();
        int len = datagramPacket.getLength();
        String receivedData = new String(buffer, 0, len, StandardCharsets.UTF_8);
        // 2、处理具体业务
        log.info("从{}:{} 接收的数据为:{} ==> 开始执行业务",
                datagramPacket.getAddress().getHostAddress(), datagramPacket.getPort(), receivedData);
        // 3、封装给客户端返回的数据包,并发送给客户端
        // 3.1、构造响应数据包
        InetAddress clientAddress = datagramPacket.getAddress();
        int clientPort = datagramPacket.getPort();
        String response = receivedData + " Received your message!";
        byte[] responseData = response.getBytes();

        DatagramPacket responsePacket = new DatagramPacket(responseData, responseData.length, clientAddress, clientPort);
        DatagramSocket datagramSocket = null;
        try {
            datagramSocket = new DatagramSocket();
            // 发送响应数据包
            datagramSocket.send(responsePacket);
            // 重置packet的偏移量和长度,以便下一次接收
            datagramPacket.setLength(buffer.length);
        } catch (Exception e) {
            log.error("UdpProcessTask消费异常:{}", e);
        } finally {
            if (datagramSocket != null) {
                datagramSocket.close();
            }
        }
    }
}

2.2.4、UdpConfig

java 复制代码
package com.zim.udp;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.net.SocketException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author zim
 */
@Configuration
public class UdpConfig {

    /**
     * 处理 udp 监听业务的线程池
     *
     * @return
     */
    @Bean
    public ExecutorService udpExecutor() {
        // 创建一个固定大小的线程池
        ExecutorService executor = new ThreadPoolExecutor(4, 4,
                0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
        return executor;
    }


    @Bean
    public Runnable udpListener(ExecutorService executor) throws SocketException {
        // 这里是监听的端口号,一般写在配置文件中
        int port = 5621;
        UdpListener udpListener = new UdpListener(port, executor);
        // 使用非守护线程来运行监听器,因为守护线程可能会在Spring Boot关闭时立即退出
        Thread thread = new Thread(udpListener, "UDP-Listener-Thread");
        // 设置为守护线程,以便在 Springboot 应用停止时自动退出
        thread.start();
        // 返回 Runnable 符合@Bean的返回类型,实际上不需要 Spring 容器管理该 Runnable
        return udpListener;
    }

}

2.2.5、执行日志

UdpClient 执行日志:

java 复制代码
21:20:07.101 [main] INFO com.zim.udp.UdpClient - this is a udpMessage->已发送
21:20:07.105 [main] INFO com.zim.udp.UdpClient - this is a udpMessage Received your message!->已接收

服务端执行日志:

java 复制代码
2024-08-22 21:20:07.099  INFO 9432 --- [pool-1-thread-4] com.zim.udp.UdpProcessTask               : 从127.0.0.1:55089 接收的数据为:this is a udpMessage ==> 开始执行业务

.

相关推荐
海绵波波10712 小时前
Webserver(4.8)UDP、广播、组播
单片机·网络协议·udp
橘色的喵14 小时前
Linux编程:DMA增加UDP 数据传输吞吐量并降低延迟
linux·udp·dma·网络驱动·低延迟·吞吐量·nic
C++忠实粉丝1 天前
计算机网络socket编程(1)_UDP网络编程实现echo server
linux·服务器·网络·c++·网络协议·计算机网络·udp
重生之我是数学王子2 天前
网络编程 UDP编程 Linux环境 C语言实现
linux·c语言·开发语言·网络·网络协议·udp
代码魔法师Sunny2 天前
4.WebSocket 配置与Nginx 的完美结合
websocket·网络协议
kunkun1013 天前
关于Websocket
网络·websocket·网络协议
flying robot3 天前
websocket的使用
websocket
Lingwl4 天前
Node.js UDP通信 dgram 组播
前端·网络·网络协议·udp·node.js
Selina K4 天前
使用UDP实现发送和接收
qt·网络协议·udp
明月看潮生4 天前
青少年编程与数学 02-003 Go语言网络编程 07课题、客户端服务器模型
服务器·青少年编程·golang·网络编程·编程与数学