JavaSocket 网络编程之 UDP
- [1、关于 UDP 协议](#1、关于 UDP 协议)
-
- [1.1、什么是 UDP 协议](#1.1、什么是 UDP 协议)
- [1.2、UDP 协议的优缺点](#1.2、UDP 协议的优缺点)
- [1.3、UDP 协议的应用场景](#1.3、UDP 协议的应用场景)
- [1.4、UDP 与 TCP 的区别](#1.4、UDP 与 TCP 的区别)
- 2、编码示例
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 ==> 开始执行业务
.