使用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

相关推荐
陈工大模型17 分钟前
2026年9月AI可见度监测工具横评:从采样一致性与中立性出发的技术选型笔记
大数据·人工智能·笔记
高山有多高38 分钟前
【Linux笔记】UpdSocket
linux·运维·笔记
RickyWasYoung1 小时前
二阶魔方底层逻辑分析
笔记
Sammyyyyy2 小时前
结构化决策模型 Jev 实战笔记,3 个原语、4 个限制、5 个落地场景
人工智能·笔记·开发工具·编程语言
东方芷兰2 小时前
Agent 技术摘要 05 —— BP、SaaS、B2B、C2B、B2C
java·笔记·python·语言模型·自然语言处理
Chen—LSN2 小时前
数据结构——拿捏复杂度
java·c语言·开发语言·数据结构·经验分享·笔记·算法
星恒随风2 小时前
Linux开发工具详解(一):软件包管理、Vim、GCC/G++、Makefile与进度条实战
linux·运维·笔记·学习·vim
小小、码农3 小时前
Ubuntu / Linux 软件安装完整笔记
linux·笔记·ubuntu
xzal123 小时前
Python 列表操作笔记:append / extend / + / pop
笔记·python
维克兜率天3 小时前
【维克】价差交易实战:配对交易的核心参数设置
大数据·笔记·算法·量化