Skip to content

Netty 网络编程 ​

Java 高级 · 底层原理

Netty 是支撑 Dubbo、RocketMQ、Elasticsearch 的底层通信框架。它的"快"不是玄学,而是三个设计的合力:主从 Reactor 多线程模型、单线程串行无锁的 EventLoop、零拷贝的 ByteBuf。本文把网络线程模型、Pipeline 责任链、字节容器与粘包拆包讲透,帮你写出既快又稳的长连接服务。

一、Reactor 模型:为什么 Netty 能扛高并发 ​

传统 BIO 一个连接一个线程,C10K 直接打爆。Netty 用主从 Reactor 多线程模型:

  • BossGroup(Main Reactor):通常 1 个 NioEventLoop,只负责 accept 接收新连接,把 Channel 注册到 Worker。
  • WorkerGroup(Sub Reactor):多个 NioEventLoop,每个绑定一个线程,负责若干 Channel 的读/写事件。

1.1 EventLoop:单线程串行的秘密 ​

每个 EventLoop 是一个死循环 run():

java
// NioEventLoop.run 逻辑精简
protected void run() {
    for (;;) {
        strategy = select(wakenUp.getAndSet(false)); // 多路复用, 等 IO 事件
        if (strategy > 0) processSelectedKeys();      // 处理就绪的 SelectionKey
        runAllTasks();                                 // 执行任务队列(用户提交/定时)
    }
}

关键设计:一个 Channel 的整个生命周期只被一个 EventLoop 线程处理(Channel 与 EventLoop 用 EventLoopGroup.register 绑定后不再变)。因此:

  • 同一个 Channel 的 channelRead、write、handlerAdded 全部在同一个线程串行执行,Handler 里不需要加锁就能保证线程安全。
  • 没有锁竞争 = 极高的吞吐。代价是:绝不能在该线程里做阻塞/耗时操作,否则这条 Channel 乃至同 EventLoop 上的所有 Channel 都卡住。

DANGER

在 Handler 里做阻塞调用(DB 查询、同步 RPC、大对象序列化)是 Netty 头号性能杀手。EventLoop 线程被阻塞 → 该线程上所有 Channel 的读写全部停滞 → 超时、连接堆积。耗时任务必须 eventLoop.execute(() -> businessPool.submit(...)) 抛到独立业务线程池,处理完再 ctx.write 写回(写回会由原 EventLoop 串行执行)。

二、ChannelPipeline:双向链表的责任链 ​

每个 Channel 持有一条 ChannelPipeline,本质是双向链表,串起一串 ChannelHandler。入站(Inbound:连接、读)事件从头(head)向尾(tail)流动;出站(Outbound:写、flush、close)从尾向头反向流动。

  • ChannelInboundHandler:处理入站(如解码、业务)。
  • ChannelOutboundHandler:处理出站(如编码)。
  • ctx.fireChannelRead(msg):把入站事件往后传;ctx.write(msg) 从当前节点往后(出站方向)传;channel.write() 从 tail 开始传。两者方向差异是初学者易错点。

SimpleChannelInboundHandler 之所以"自动释放"入站 ByteBuf,是因为它在 channelRead 调用完你的 channelRead0 后会帮你 ReferenceCountUtil.release(msg)——但如果你 retain() 了或转交给别的线程,就得自己负责释放。

三、ByteBuf:零拷贝与引用计数 ​

Netty 自研的 ByteBuf 相比 JDK ByteBuffer 做了多项改进:

  • 读写双指针:readerIndex / writerIndex 分离,读不影响写指针,省去 flip() 的繁琐。
  • 动态扩容:写超容量自动扩容,类似 StringBuilder。
  • 池化(PooledByteBufAllocator):基于 Netty 的 jemalloc 风格内存池分配,减少 GC 压力(默认开启)。
  • 零拷贝:
    • slice() / duplicate():生成共享底层内存的视图,不复制数据。
    • CompositeByteBuf:把多个 ByteBuf 逻辑拼成一个,避免把多段数据 copy 到一起再发。
    • FileRegion:基于 FileChannel.transferTo,调用 OS 的 sendfile,实现文件零拷贝传输。
  • 引用计数:ByteBuf 实现 ReferenceCounted,retain() 加一、release() 减一,归零即归还池/释放。Netty 用 -Dio.netty.leakDetection.level=PARANOID(开发)检测未释放导致的内存泄漏。
java
// ByteBuf 基本读写: 读写指针分离
ByteBuf buf = PooledByteBufAllocator.DEFAULT.directBuffer(1024);
buf.writeInt(1);          // writerIndex += 4
buf.writeBytes(payload);  // 追加
int v = buf.readInt();    // readerIndex += 4, 数据未动, 仅移指针
// 用完必须 release (或交给 SimpleChannelInboundHandler 自动释放)
buf.release();

四、TCP 粘包/拆包与 LengthField 解码 ​

TCP 是字节流协议,没有消息边界。发送端多次 write 可能被合并成一个包(粘包),一个大包也可能被拆成多个(拆包)。必须在应用层定界。Netty 提供解码器:

  • LineBasedFrameDecoder:以换行符 \n 定界(适合文本行协议)。
  • DelimiterBasedFrameDecoder:自定义分隔符。
  • LengthFieldBasedFrameDecoder:最通用——在协议头放一个"长度字段",解码器据此截取完整包。
java
// 协议: [长度(4字节, 大端)][body]
// lengthFieldOffset=0(长度域在开头), lengthFieldLength=4, lengthAdjustment=0, initialBytesToStrip=4(解码后去掉长度头)
pipeline.addLast(new LengthFieldBasedFrameDecoder(1024 * 1024, 0, 4, 0, 4));
pipeline.addLast(new StringDecoder());

// 对应编码器
public class LengthFieldEncoder extends MessageToByteEncoder<String> {
    @Override protected void encode(ChannelHandlerContext ctx, String msg, ByteBuf out) {
        byte[] data = msg.getBytes(StandardCharsets.UTF_8);
        out.writeInt(data.length);     // 先写长度
        out.writeBytes(data);          // 再写 body
    }
}

五、IdleState 心跳:清理半开连接 ​

网络故障、客户端崩溃会导致"半开连接"(服务端以为还连着,实际已断)。IdleStateHandler 检测读/写空闲时长,触发 userEventTriggered,应用可主动关连接或发 ping/pong:

java
pipeline.addLast(new IdleStateHandler(60, 0, 0, TimeUnit.SECONDS)); // 60s 读空闲
// 业务 Handler 内
@Override public void userEventTriggered(ChannelHandlerContext ctx, Object evt) {
    if (evt instanceof IdleStateEvent) {
        ctx.close(); // 读空闲超时, 关闭疑似死连接
    }
}

六、生产最佳实践 ​

  • boss 线程数设 1,worker 默认 CPU*2;耗时逻辑一律外抛业务线程池。
  • 用 LengthFieldBasedFrameDecoder 做定长协议 + MessageToByteEncoder 编码。
  • 开启 IdleStateHandler 心跳;开发期 leakDetection.level=PARANOID,生产降 SIMPLE。
  • 启用 PooledByteBufAllocator(默认)并合理设 RecvByteBufAllocator 自调节接收缓冲。

📚 官方文档与参考资料 ​

✏️ 练习与自测 ​

  1. 画出主从 Reactor 模型中 Boss/Worker EventLoop 的职责划分与单线程串行如何处理多连接,说明为何 Handler 内不需加锁。
  2. 用 LengthFieldBasedFrameDecoder 设计一个"长度(4 字节大端) + body"的私有协议,编写对应 Encoder 并推演粘包场景的解码过程。
  3. 解释 SimpleChannelInboundHandler 自动释放 ByteBuf 的原理,何时必须用 ReferenceCountUtil.retain 或转移给别的线程。
  4. 设计一个方案:Handler 内要做 200ms 的阻塞 RPC,如何既利用 Netty 又不卡 EventLoop 线程。

本节小结

Netty 的高性能源于"主从 Reactor 多线程 + EventLoop 单线程串行无锁 + ByteBuf 零拷贝/池化/引用计数"。掌握线程模型(耗时外抛)、Pipeline 责任链(入站出站反向)、粘包处理(LengthField 解码)与心跳保活(IdleState)四件事,配合"ByteBuf 善后、引用计数不漏"的纪律,才能写出稳定长连接服务。