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():
// 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(开发)检测未释放导致的内存泄漏。
// 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:最通用——在协议头放一个"长度字段",解码器据此截取完整包。
// 协议: [长度(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:
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自调节接收缓冲。
📚 官方文档与参考资料
- Netty 官方 Wiki:https://netty.io/wiki/
- Netty 官方站:https://netty.io/
- Netty 用户指南(4.x):https://netty.io/wiki/user-guide-for-4.x.html
✏️ 练习与自测
- 画出主从 Reactor 模型中 Boss/Worker EventLoop 的职责划分与单线程串行如何处理多连接,说明为何 Handler 内不需加锁。
- 用
LengthFieldBasedFrameDecoder设计一个"长度(4 字节大端) + body"的私有协议,编写对应 Encoder 并推演粘包场景的解码过程。 - 解释
SimpleChannelInboundHandler自动释放ByteBuf的原理,何时必须用ReferenceCountUtil.retain或转移给别的线程。 - 设计一个方案:Handler 内要做 200ms 的阻塞 RPC,如何既利用 Netty 又不卡 EventLoop 线程。
本节小结
Netty 的高性能源于"主从 Reactor 多线程 + EventLoop 单线程串行无锁 + ByteBuf 零拷贝/池化/引用计数"。掌握线程模型(耗时外抛)、Pipeline 责任链(入站出站反向)、粘包处理(LengthField 解码)与心跳保活(IdleState)四件事,配合"ByteBuf 善后、引用计数不漏"的纪律,才能写出稳定长连接服务。