JavaSocket 网络编程之 UDP
JavaSocket 网络编程之 UDP
- 1、关于 UDP 协议
- 1.1、什么是 UDP 协议
- 1.2、UDP 协议的优缺点
- 1.3、UDP 协议的应用场景
- 1.4、UDP 与 TCP 的区别
- 2、编码示例
- 2.1、说明
- 2.2、代码示例
- 2.2.1、UdpClient
- 2.2.2、UdpListener
- 2.2.3、UdpProcessTask
- 2.2.4、UdpConfig
- 2.2.5、执行日志
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.1InetAddress 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;}@Overridepublic 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;}/*** 拿到数据包,处理相关业务*/@Overridepublic 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*/@Beanpublic ExecutorService udpExecutor() {// 创建一个固定大小的线程池ExecutorService executor = new ThreadPoolExecutor(4, 4,0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());return executor;}@Beanpublic 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 容器管理该 Runnablereturn 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 ==> 开始执行业务
.
