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

 

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

 

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

 

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

 

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 执行日志:

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!->已接收

 
服务端执行日志:

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 ==> 开始执行业务

 
 
 
 
 
 
 
.

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值