程序的结构如下:

1)NettyUdpServer类,这里封装服务端启动与管理,里面内置监听2个端口,启动一个线程池,池的长度设置为20万,在池内执行数据的解析与转发处理,代码如下:
java
public class NettyUdpServer {
private static final Logger LOG = GlobalLogger.getLogger();
// ========== 可配置常量(便于调整) ==========
/** UDP接收端口1 */
public static final int UDP_PORT_1 = 8000;
/** UDP接收端口2 */
public static final int UDP_PORT_2 = 9000;
/** 接收/发送缓冲区大小(字节)- 20MB */
public static final int UDP_BUFFER_SIZE = 20 * 1024 * 1024;
/** 线程池队列容量(改为10万) */
public static int THREAD_POOL_QUEUE_CAPACITY = 100000;
/** 线程池核心线程数 */
public static final int CORE_POOL_SIZE = 5;
/** 线程池最大线程数 */
public static final int MAX_POOL_SIZE = 10;
// ========== 核心组件 ==========
private final ThreadPoolExecutor workerPool;
// 每个端口一个netty接收线程
private final EventLoopGroup group1 = new NioEventLoopGroup(1);
private final EventLoopGroup group2 = new NioEventLoopGroup(1);
private Channel channel1;
private Channel channel2;
private volatile boolean isRunning = true;
// ========== 构造函数 ==========
public NettyUdpServer() {
// 初始化线程池(队列容量改为10万,拒绝策略改为AbortPolicy)
this.workerPool = new ThreadPoolExecutor(
CORE_POOL_SIZE, MAX_POOL_SIZE,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(THREAD_POOL_QUEUE_CAPACITY), // 队列容量10万
r -> {
Thread t = new Thread(r, "udp-worker-" + Thread.currentThread().getId());
t.setDaemon(false);
return t;
},
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略改为抛出异常
);
}
// 对外提供线程池获取方法(供Handler使用)
public ThreadPoolExecutor getWorkerPool() {
return workerPool;
}
// ========== 启动单个UDP端口服务 ==========
private void startSingleUdpServer(int port, EventLoopGroup group) throws Exception {
Bootstrap bootstrap = new Bootstrap();
bootstrap.group(group)
.channel(NioDatagramChannel.class)
// 核心:开启端口复用(SO_REUSEADDR)
.option(ChannelOption.SO_REUSEADDR, true)
// 支持广播
.option(ChannelOption.SO_BROADCAST, true)
// 接收缓冲区大小
.option(ChannelOption.SO_RCVBUF, UDP_BUFFER_SIZE)
// 发送缓冲区大小
.option(ChannelOption.SO_SNDBUF, UDP_BUFFER_SIZE)
.handler(new UdpPacketHandler(port, this)); // 传入Server实例,用于获取线程池
// 绑定端口
ChannelFuture future = bootstrap.bind(port).sync();
if (future.isSuccess()) {
if (port == UDP_PORT_1) {
channel1 = future.channel();
} else if (port == UDP_PORT_2) {
channel2 = future.channel();
}
System.out.println(String.format("UDP服务启动成功 - 端口:%d,缓冲区大小:%dMB",
port, UDP_BUFFER_SIZE / 1024 / 1024));
} else {
throw new Exception("端口" + port + "绑定失败:" + future.cause().getMessage());
}
}
// ========== 核心:UDP服务启动函数 ==========
public void startUdpServerBeforeWeb() throws Exception {
System.out.println("开始启动UDP服务...");
// 1. 先启动1端口
startSingleUdpServer(UDP_PORT_1, group1);
// 2. 再启动2端口
startSingleUdpServer(UDP_PORT_2, group2);
// 3. 验证UDP服务是否全部启动成功
if (channel1 == null || !channel1.isActive()
|| channel2 == null || !channel2.isActive()) {
throw new Exception("UDP服务启动不完整,部分端口未绑定成功");
}
System.out.println("UDP服务全部启动完成,可启动Web服务器");
}
// ========== 优雅关闭 ==========
public void stop() {
isRunning = false;
// 关闭通道
if (channel1 != null) channel1.close().awaitUninterruptibly();
if (channel2 != null) channel2.close().awaitUninterruptibly();
// 关闭Netty线程组
group1.shutdownGracefully().awaitUninterruptibly();
group2.shutdownGracefully().awaitUninterruptibly();
// 关闭线程池
workerPool.shutdown();
try {
if (!workerPool.awaitTermination(30, TimeUnit.SECONDS)) {
workerPool.shutdownNow();
}
} catch (InterruptedException e) {
workerPool.shutdownNow();
}
System.out.println("UDP服务已优雅关闭");
}
// ========== 测试:模拟Web服务器启动流程 ==========
public static void main(String[] args) {
NettyUdpServer udpServer = new NettyUdpServer();
try {
// 1. 启动资源监控器
UdpResourceMonitor monitor = new UdpResourceMonitor();
monitor.start();
// 第一步:启动UDP服务(必须先执行)
udpServer.startUdpServerBeforeWeb();
// 第二步:启动Netty Web服务器(示例伪代码)
System.out.println("开始启动Netty Web服务器...");
// WebServer.start(); // 你的Web服务器启动函数
System.out.println("Netty Web服务器启动完成");
// 阻塞主线程
System.in.read();
} catch (Exception e) {
System.err.println("启动失败:" + e.getMessage());
udpServer.stop(); // 启动失败关闭UDP服务
} finally {
udpServer.stop();
}
}
}
- 2个端口都有隶属于单独的一个监听工作组,收到数据包以后,使用
UdpPacketHandler 来处理数据,简单的封装一下,放到内存池中,代码如下:
java
/**
* UDP数据包处理器(改造点:
* 1. 去掉独立队列,直接提交任务到线程池
* 2. 检查线程池队列长度
* 3. 处理线程池拒绝异常
*/
public class UdpPacketHandler extends SimpleChannelInboundHandler<DatagramPacket> {
private static final Logger LOG = GlobalLogger.getLogger();
private final int listenPort; // 当前监听的端口
private final NettyUdpServer udpServer; // 引用Server获取线程池
private final ThreadPoolExecutor workerPool; // 线程池实例
// 队列容量阈值(80%),用于预警
private static final float QUEUE_WARN_THRESHOLD = 0.8f;
// 构造方法:接收端口和Server实例
public UdpPacketHandler(int port, NettyUdpServer server) {
this.listenPort = port;
this.udpServer = server;
this.workerPool = server.getWorkerPool(); // 获取线程池
}
// 提交失败:Handler 的 catch 块手动调用 copiedData.release() → 计数从 2→1,最终由 Netty 自动减到 0;
// 提交成功:业务层执行 udpData.releaseUdpData() → 计数从 2→1,最后剩下的 1 由 Netty IO 线程自动释放(计数 1→0),完成内存回收。
@Override
protected void channelRead0(ChannelHandlerContext ctx, DatagramPacket packet) {
// 1. 拷贝原始数据(引用计数=1)
ByteBuf copiedData = packet.content().copy();
InetSocketAddress clientAddr = packet.sender();
String clientIp = clientAddr.getAddress().getHostAddress(); // 客户端IP
int clientSrcPort = clientAddr.getPort(); // 客户端发送数据的源端口(重点)
// 标记是否成功提交到线程池
boolean isTaskSubmitted = false;
int queueCapacity = NettyUdpServer.THREAD_POOL_QUEUE_CAPACITY;
try {
// 2. 增加引用计数(计数=2),确保封装后不被Netty自动回收
ByteBuf retainedData = copiedData.retain();
// 3. 封装为UdpData(业务层将通过这个对象释放资源)
UdpData udpData = new UdpData(retainedData, clientAddr, listenPort);
// 队列预警
int queueSize = workerPool.getQueue().size();
if (queueSize > queueCapacity * QUEUE_WARN_THRESHOLD) {
System.err.printf("[预警] 线程池队列接近满 - 服务端端口:%d,客户端:%s:%d,队列长度:%d/%d%n",
listenPort, clientIp, clientSrcPort, queueSize, queueCapacity);
}
// 4. 提交任务到线程池(仅传递数据,不处理释放)
workerPool.submit(() -> {
try {
// 业务层处理数据 + 主动释放资源
UdpPacketParser.processUdpData(udpData, ctx.channel());
} catch (Exception e) {
System.err.printf("[处理异常] 服务端端口:%d,客户端:%s:%d,原因:%s%n",
listenPort, clientIp, clientSrcPort, e.getMessage());
// 兜底:如果业务层抛出异常未释放,这里补充释放
udpData.safeRelease();
}
// 注意:这里不再写finally释放,核心释放逻辑在业务层
});
isTaskSubmitted = true;
} catch (RejectedExecutionException e) {
// 线程池拒绝任务:释放1次(计数=2→1)
// 格式化字符串:%d(服务端端口)、%s:%d(客户端IP+端口)、%d(队列长度)、%d(队列容量) → 共5个参数
System.err.printf("[任务拒绝] 服务端端口:%d,客户端:%s:%d,队列长度:%d/%d%n",
listenPort, clientIp, clientSrcPort, workerPool.getQueue().size(), queueCapacity);
if (copiedData != null && copiedData.refCnt() > 0) {
copiedData.release();
}
} catch (Exception e) {
// 其他异常:释放1次(计数=2→1 或 1→0)
// 格式化字符串:%d(服务端端口)、%s:%d(客户端IP+端口)、%s(异常原因) → 共4个参数
System.err.printf("[数据包异常] 服务端端口:%d,客户端:%s:%d,原因:%s%n",
listenPort, clientIp, clientSrcPort, e.getMessage());
if (copiedData != null && copiedData.refCnt() > 0) {
copiedData.release();
}
} finally {
// 任务提交成功 → 资源交给业务层释放1次,这里应该是2-1
// 任务提交失败 → 已在catch中释放1次,这里就是1-1
if (copiedData != null && copiedData.refCnt() > 0) {
copiedData.release();
System.out.printf("[FINALLY] 释放后计数:%d%n", copiedData.refCnt()); // 可选:验证计数
}
}
}
@Override
public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
System.err.printf("端口%d接收异常:%s%n", listenPort, cause.getMessage());
// 仅打印异常,不关闭通道(避免单个异常导致整个UDP服务挂掉)
// ctx.close();
}
}
- 使用一个简单的类封装数据包,用于解析时候使用:
java
public class UdpData {
// 核心字段(final 保证不可变,线程安全)
private final ByteBuf data; // 接收的原始数据
private final InetSocketAddress clientAddr; // 客户端地址
private final int port; // 接收端口(8090/8099)
/**
* 构造函数(增加空值校验,避免 NPE)
* @param data 原始UDP数据(ByteBuf)
* @param clientAddr 客户端地址
* @param port 接收端口
*/
public UdpData(ByteBuf data, InetSocketAddress clientAddr, int port) {
// 空值校验:ByteBuf 为空时直接抛异常,避免后续操作 NPE
if (data == null) {
throw new IllegalArgumentException("ByteBuf data cannot be null!");
}
this.data = data;
this.clientAddr = clientAddr;
this.port = port;
}
- 内存池中中的数据处理与应答等,在
UdpPacketParser 类的静态函数中封装,
java
**
* 这个类在线程池中工作,由消费者推入
* UDP数据包解析&处理工具类
* 职责:纯静态工具方法,负责数据包解析、业务处理、应答发送
* 特点:无状态、可复用、与Netty核心逻辑解耦
* Netty 的 ByteBuf 所有数值类型的读写方法(readShort()/writeShort()、readInt()/writeInt() 等),
* 默认都是大端序(BIG_ENDIAN),这是为了遵循「网络字节序」的行业标准(TCP/UDP 协议都使用大端序)。
*/
public class UdpPacketParser {
// 替换System.out/err为SLF4J日志(生产环境推荐)
private static final Logger LOG = GlobalLogger.getLogger();
/**
* 处理UDP数据包(核心方法)
* @param udpData 封装的UDP数据对象
* @param channel 发送应答的Netty通道(可为null)
*/
public static void processUdpData(UdpData udpData, Channel channel) {
// 1. 空值校验(避免NPE)
if (udpData == null) {
LOG.warn("处理UDP数据失败:UdpData为null");
return;
}
ByteBuf dataBuf = udpData.getData();
if (dataBuf == null) {
LOG.warn("处理UDP数据失败:数据缓冲区为null - 客户端:{},端口:{}",
udpData.getClientAddr(), udpData.getPort());
return;
}
try {
// 2. 解析数据(可扩展为更复杂的协议解析)
String dataStr = parseByteBufToString(dataBuf);
InetSocketAddress clientAddr = udpData.getClientAddr();
int port = udpData.getPort();
LOG.info("开始处理UDP数据 - 端口:{},客户端:{},数据长度:{},数据内容:{}",
port, clientAddr, dataBuf.readableBytes(), dataStr);
// 3. 自定义业务逻辑(TODO:替换为你的实际业务)
String responseContent = businessProcess(udpData, dataStr);
LOG.debug("处理后返回的数据:" + responseContent);
// 4. 发送应答(独立方法,便于复用/修改)
sendResponse(udpData, channel, responseContent);
} catch (Exception e) {
LOG.error("处理UDP数据异常 - 客户端:{},端口:{}",
udpData.getClientAddr(), udpData.getPort(), e);
} finally {
// 5. 安全释放资源(防止重复释放)
udpData.safeRelease();
}
}
// ========== 拆分独立方法:便于扩展和单元测试 ==========
/**
* 解析ByteBuf为字符串(可扩展为二进制/Protobuf等解析)
*/
// private static String parseByteBufToString(ByteBuf dataBuf) {
// // 标记读指针位置,解析后恢复(避免影响后续操作)
// dataBuf.markReaderIndex();
// try {
// return dataBuf.toString(StandardCharsets.UTF_8);
// } finally {
// dataBuf.resetReaderIndex(); // 恢复读指针
// }
// }
/**
* 自定义业务逻辑(单独抽离,便于维护)
*/
private static String businessProcess(UdpData udpData, String dataStr) {
// TODO:替换为你的实际业务逻辑(比如入库、调用接口、数据校验等)
int port = udpData.getPort();
return String.format("端口%d处理成功:%s", port, dataStr);
}
// 替换 parseByteBufToString 方法
private static String parseByteBufToString(ByteBuf dataBuf) {
dataBuf.markReaderIndex();
try {
// 小端序读取(仅特殊场景使用,比如和某些设备通信)
//short msgType = dataBuf.readShortLE();
// 示例:二进制协议解析(前2字节是消息类型,后4字节是长度,剩余是内容)
short msgType = dataBuf.readShort();
short contentLen = dataBuf.readShort();
byte[] contentBytes = new byte[contentLen];
dataBuf.readBytes(contentBytes);
//System.out.println( msgType);
//System.out.println( contentLen);
//System.out.println(new String(contentBytes, StandardCharsets.UTF_8));
return String.format("消息类型:%d,内容:%s", msgType, new String(contentBytes, StandardCharsets.UTF_8));
} finally {
dataBuf.resetReaderIndex();
}
}
/**
* 发送UDP应答(独立方法,便于统一修改发送逻辑)
*/
private static void sendResponse(UdpData udpData, Channel channel, String responseContent) {
if (channel == null || !channel.isActive()) {
LOG.warn("发送应答失败:通道未激活 - 客户端:{},端口:{}",
udpData.getClientAddr(), udpData.getPort());
return;
}
// ========== 按约定格式封装应答 ==========
ByteBuf originalData = udpData.getData();
// 1. 读取原字符串内容(如果是按"short+short+字符串"格式发送的)
originalData.markReaderIndex(); // 标记读指针,避免影响后续操作
short msgType = originalData.readShort(); // 原类型
short contentLen = originalData.readShort(); // 原长度
byte[] contentBytes = new byte[contentLen];
originalData.readBytes(contentBytes);
String originalContent = new String(contentBytes, StandardCharsets.UTF_8);
originalData.resetReaderIndex(); // 恢复读指针
// 2. 封装应答格式:short(类型2) + short(新长度) + 字符串(原内容/自定义内容)
String respContent = "echo: " + originalContent; // 自定义应答内容
byte[] respBytes = respContent.getBytes(StandardCharsets.UTF_8);
short respType = 2; // 应答类型
short respLen = (short) respBytes.length;
// 3. 创建缓冲区并写入数据(大端序,和Python客户端匹配)
ByteBuf responseBuf = Unpooled.buffer(4 + respLen); // 2+2+内容长度
responseBuf.writeShort(respType); // 大端序写short
responseBuf.writeShort(respLen); // 大端序写short
responseBuf.writeBytes(respBytes); // 写字符串内容
// 4. 封装并发送
sendPacket(udpData, channel, respType, responseBuf, respContent);
}
// 执行封装与发送
private static void sendPacket(UdpData udpData, Channel channel, short respType, ByteBuf responseBuf, String respContent) {
InetSocketAddress clientAddr = udpData.getClientAddr();
DatagramPacket responsePacket = new DatagramPacket(responseBuf, clientAddr);
channel.writeAndFlush(responsePacket).addListener(future -> {
if (future.isSuccess()) {
LOG.info("应答发送成功 - 对端:{},本地端口:{}, 应答类型:{}, 应答内容{}",
udpData.getRemoteIp(), udpData.getPort(), respType, respContent);
} else {
LOG.error("应答发送失败 - 客户端:{},端口:{}", clientAddr, udpData.getPort(), future.cause());
// 可选:失败后重试1次(避免缓冲区满的临时异常)
retrySend(clientAddr, responsePacket, channel, 1);
}
});
}
// 可选:简单的重试逻辑
private static void retrySend(InetSocketAddress clientAddr, DatagramPacket packet, Channel channel, int retryCount) {
if (retryCount > 3) { // 最多重试3次
LOG.error("UDP应答重试次数耗尽 - 客户端:{}:{}", clientAddr.getAddress().getHostAddress(), clientAddr.getPort());
return;
}
packet.content().retain(); // 重试前retain保活ByteBuf
channel.writeAndFlush(packet).addListener(future -> {
if (!future.isSuccess()) {
LOG.warn("UDP应答重试{}次失败,将继续重试", retryCount);
retrySend(clientAddr, packet, channel, retryCount + 1);
}
});
}
}
这里是封装了一个简单的数据包:short 类型,short长度,字符串,使用大端来发送和接收的;
后续根据业务的需求,直接改静态函数就可以了,而服务类基本不需要改动,从配置设置几个参数就可以了;
5)最后写一个python的程序,向9000端口发数据并等待应答:
python
import socket
import struct
import time
from typing import Tuple
# ========== 配置常量 ==========
# 本地绑定端口(发送数据的端口)
LOCAL_BIND_PORT = 8888
# 目标UDP服务端地址和端口
TARGET_HOST = "127.0.0.1" # 本地测试用,实际替换为服务端IP
TARGET_PORT = 9000
# 超时时间(秒)
RECV_TIMEOUT = 5
# ========== 核心工具函数 ==========
def pack_udp_data(msg_type: int, content: str) -> bytes:
"""
封装UDP数据包:short(类型) + short(内容长度) + 字符串内容
:param msg_type: 消息类型(short类型,范围-32768~32767)
:param content: 字符串内容
:return: 封装后的字节数据
"""
# 1. 将字符串转为字节(UTF-8编码)
content_bytes = content.encode("utf-8")
# 2. 获取内容长度(转为short类型,注意长度不能超过short最大值32767)
content_len = len(content_bytes)
if content_len > 32767:
raise ValueError(f"内容长度({content_len})超过short类型最大值(32767)")
# 3. 打包格式:!hh 表示网络字节序(大端)的两个short
# ! = network byte order (big-endian)
# h = short (2 bytes)
header = struct.pack("!hh", msg_type, content_len)
# 4. 拼接头部和内容
return header + content_bytes
def unpack_udp_data(data: bytes) -> Tuple[int, str]:
"""
解析UDP数据包:short(类型) + short(内容长度) + 字符串内容
:param data: 接收到的字节数据
:return: (消息类型, 内容字符串)
"""
# 1. 校验数据长度(至少4字节头部)
if len(data) < 4:
raise ValueError(f"数据长度不足,无法解析:{len(data)}字节")
# 2. 解析头部(前4字节)
header = data[:4]
msg_type, content_len = struct.unpack("!hh", header)
# 3. 校验内容长度
content_bytes = data[4:]
if len(content_bytes) != content_len:
raise ValueError(f"内容长度不匹配:声明{content_len}字节,实际{len(content_bytes)}字节")
# 4. 解析内容字符串
content = content_bytes.decode("utf-8")
return msg_type, content
# ========== 主客户端逻辑 ==========
def udp_client_send_and_recv():
"""
绑定8888端口发送数据到8000端口,并等待解析应答
"""
# 1. 创建UDP socket(SOCK_DGRAM表示UDP)
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
# 2. 绑定本地8888端口(确保发送数据的源端口是8888)
sock.bind(("0.0.0.0", LOCAL_BIND_PORT))
# 3. 设置接收超时
sock.settimeout(RECV_TIMEOUT)
try:
# ===== 步骤1:构造并发送数据 =====
# 消息类型1,内容自定义
msg_type = 1
content = "Hello Netty UDP Server! 测试数据123"
# 封装数据包
send_data = pack_udp_data(msg_type, content)
# 发送到目标服务端
target_addr = (TARGET_HOST, TARGET_PORT)
sock.sendto(send_data, target_addr)
print(f"✅ 已发送数据到 {target_addr}")
print(f" 发送内容 - 类型:{msg_type},长度:{len(content.encode('utf-8'))},内容:{content}")
print(f" 发送字节:{send_data.hex()}(十六进制)")
# ===== 步骤2:等待并解析应答 =====
print("\n⏳ 等待服务端应答...")
recv_data, server_addr = sock.recvfrom(4096) # 接收缓冲区4096字节
print(f"\n✅ 收到来自 {server_addr} 的应答")
print(f" 应答字节:{recv_data.hex()}(十六进制)")
# 解析应答数据
resp_type, resp_content = unpack_udp_data(recv_data)
print(f" 应答解析 - 类型:{resp_type},内容:{resp_content}")
except socket.timeout:
print(f"\n❌ 接收超时({RECV_TIMEOUT}秒),未收到服务端应答")
except struct.error as e:
print(f"\n❌ 数据包解析失败(格式错误):{e}")
except Exception as e:
print(f"\n❌ 发送/接收失败:{e}")
# ========== 测试运行 ==========
if __name__ == "__main__":
print("=== Python UDP 客户端 ===")
print(f"本地绑定端口:{LOCAL_BIND_PORT}")
print(f"目标服务端:{TARGET_HOST}:{TARGET_PORT}")
print("-" * 50)
# 循环发送(可选,单次发送注释掉即可)
while True:
udp_client_send_and_recv()
print("-" * 50)
time.sleep(2) # 间隔2秒发送一次
- python输出的效果就是如下:
✅ 已发送数据到 ('127.0.0.1', 9000)
发送内容 - 类型:1,长度:39,内容:Hello Netty UDP Server! 测试数据123
发送字节:0001002748656c6c6f204e6574747920554450205365727665722120e6b58be8af95e695b0e68dae313233(十六进制)
⏳ 等待服务端应答...
✅ 收到来自 ('127.0.0.1', 9000) 的应答
应答字节:0002002d6563686f3a2048656c6c6f204e6574747920554450205365727665722120e6b58be8af95e695b0e68dae313233(十六进制)
应答解析 - 类型:2,内容:echo: Hello Netty UDP Server! 测试数据123