使用netty4写一个UDP的echo服务(笔记)

程序的结构如下:

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();
        }
    }
}
  1. 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();
    }
}
  1. 使用一个简单的类封装数据包,用于解析时候使用:
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;

    }
  1. 内存池中中的数据处理与应答等,在

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秒发送一次

        
  1. 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

相关推荐
sprite_雪碧2 小时前
笔记:考研机试 —— 进制转换类问题
笔记·考研
ysa0510302 小时前
运用map优化多次查询【Kadomatsu 子序列】
数据结构·c++·笔记·算法
苦瓜小生2 小时前
【黑马点评学习笔记 | 实战篇 】| 10-用户签到+UV统计
笔记·后端·学习
24白菜头2 小时前
第十五届蓝桥杯C&C++大学B组
数据结构·c++·笔记·学习·算法·leetcode·蓝桥杯
qcwl662 小时前
深入理解Linux进程与内存 学习笔记#3
linux·笔记·学习
-Springer-2 小时前
STM32 学习 —— 个人学习笔记10-1(I2C 通信协议及 MPU6050 简介 & 软件 I2C 读写 MPU6050)
笔记·stm32·学习
小陈phd2 小时前
多模态大模型学习笔记(二十二)——大模型微调全解:从全量调参到LoRA的参数高效训练实战
笔记·学习
Engineer邓祥浩2 小时前
JVM学习笔记(3) 第二部分 自动内存管理 第2章 Java内存区域与内存溢出异常
jvm·笔记·学习
inputA2 小时前
C语言可变参数(va_list、va_start、va_end、va_arg)
c语言·笔记