Netty 速通
一份写给新手的极简指南:用最少的术语、最直白的例子,搞懂 Netty 到底在干什么、怎么写、以及千万别踩的坑。
1Netty 是什么?
先有个整体印象,再抠细节。
Netty 是一个基于 Java NIO 的异步、事件驱动的网络应用框架。说人话就是:
- 它帮你用 Java 写网络通信程序(服务器 / 客户端);
- 它把底层难用的 NIO(Selector、Channel、Buffer)包成了简单好用的 API;
- 它天生高性能、高并发,很多著名项目都用它(Dubbo、RocketMQ、Elasticsearch、ZooKeeper…)。
2为什么不直接用 Java NIO?
不是不能,是太痛苦。
| 原生 Java NIO | Netty |
|---|---|
| API 繁琐,Selector / 附件管理很容易写错 | 封装成 Channel + Pipeline,心智负担低 |
| 要自己处理粘包 / 拆包 | 内置一大堆 FrameDecoder,开箱即用 |
| 断连、异常、半包要自己兜底 | 事件回调清晰(channelActive / exceptionCaught…) |
ByteBuffer 的 flip() 反人类 | ByteBuf 读写双指针,不用 flip |
| 线程模型自己搭 | 成熟的 Reactor 主从多线程模型 |
36 个核心概念(必背)
这是全文重点。把下面 6 个词记住,Netty 你就懂一半了。
Channel
一条网络连接(类似 Socket)。读写数据都通过它。客户端和服务端各持有一个。
EventLoop
"事件循环线程"。一个 EventLoop 不断轮询 I/O 事件并执行任务。连接会被绑定到固定的 EventLoop。
ChannelFuture
异步操作的结果载体。Netty 里大部分操作不立即返回结果,而是返回 Future,用 addListener 拿结果。
ChannelHandler
真正写业务逻辑的地方。入站事件用 ChannelInboundHandler,出站用 ChannelOutboundHandler。
ChannelPipeline
Handler 组成的责任链。数据进来/出去会依次流经每个 Handler,像流水线。
ByteBuf
Netty 的数据容器,替代 NIO 的 ByteBuffer。读写指针分离,用起来舒服得多。
它们怎么串起来?
Channel 绑定 EventLoop,数据用 ByteBuf 装,沿 Pipeline 上的 Handler 流转
4线程模型:Reactor
不用背名词,看图就懂。Netty 用的是"主从 Reactor 多线程"。
- Boss 线程(1 个或几个):只负责accept 新连接,拿到连接就甩给 Worker。
- Worker 线程(一组):负责已连接通道的读写、编解码、业务处理。
- 每个连接固定绑定一个 Worker 线程 → 无锁串行,既快又不容易出并发 bug。
5第一个 Netty 服务端
照着抄一遍,跑通就有感觉了。
// 服务端启动:绑定端口、配置线程、装配 Pipeline public class ServerDemo { public static void main(String[] args) throws Exception { EventLoopGroup boss = new NioEventLoopGroup(1); // 接收连接 EventLoopGroup worker = new NioEventLoopGroup(); // 处理连接 try { ServerBootstrap b = new ServerBootstrap(); b.group(boss, worker) .channel(NioServerSocketChannel.class) .childHandler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new StringDecoder()); // 字节→字符串 ch.pipeline().addLast(new StringEncoder()); // 字符串→字节 ch.pipeline().addLast(new MyServerHandler()); } }); ChannelFuture f = b.bind(8080).sync(); // 绑定端口,阻塞等待 System.out.println("服务端启动,端口 8080"); f.channel().closeFuture().sync(); // 等待关闭 } finally { boss.shutdownGracefully(); worker.shutdownGracefully(); } } }
// 业务 Handler:收到消息 → 打印 → 回一句 class MyServerHandler extends ChannelInboundHandlerAdapter { // 客户端发来消息时触发 @Override public void channelRead(ChannelHandlerContext ctx, Object msg) { String text = (String) msg; System.out.println("收到:" + text); ctx.writeAndFlush("服务器说:收到啦!\n"); } // 连接建立时触发 @Override public void channelActive(ChannelHandlerContext ctx) { ctx.writeAndFlush("欢迎连接 Netty 服务端!\n"); } // 出异常一定要处理,否则连接会"悄悄"泄漏 @Override public void exceptionCaught(ChannelHandlerContext ctx, Throwable e) { e.printStackTrace(); ctx.close(); } }
ServerBootstrap 管服务端、Bootstrap 管客户端;group(boss, worker) 就是第 4 节那个主从线程模型。6第一个 Netty 客户端
和服务端几乎一样,只是换成 Bootstrap、只配一个线程组。
public class ClientDemo { public static void main(String[] args) throws Exception { EventLoopGroup group = new NioEventLoopGroup(); try { Bootstrap b = new Bootstrap(); b.group(group) .channel(NioSocketChannel.class) .handler(new ChannelInitializer<SocketChannel>() { protected void initChannel(SocketChannel ch) { ch.pipeline().addLast(new StringDecoder()); ch.pipeline().addLast(new StringEncoder()); ch.pipeline().addLast(new MyClientHandler()); } }); ChannelFuture f = b.connect("localhost", 8080).sync(); f.channel().writeAndFlush("你好,Netty!\n"); // 发一句话 f.channel().closeFuture().sync(); } finally { group.shutdownGracefully(); } } }
7编解码器(Codec):解决粘包 / 拆包
新手第二个最容易翻车的地方。TCP 是"流",没有消息边界。
TCP 会把你的数据随意拼接或拆分后发送。比如你发了两条 "A\n" "B\n",对方可能一次收到 "AB\n"(粘包),也可能先收到 "A" 再收到 "B\n"(拆包)。
正确做法:用 Netty 内置的"按边界切包"解码器,放在 Pipeline 最前面:
| 解码器 | 切包规则 | 适用场景 |
|---|---|---|
LineBasedFrameDecoder | 以 \n 或 \r\n 为一条消息边界 | 文本行协议(上面例子用这个最省事) |
DelimiterBasedFrameDecoder | 自定义分隔符 | 有特殊结尾符的协议 |
LengthFieldBasedFrameDecoder | 消息头里带长度字段 | 二进制协议(最常用、最严谨) |
// 正确的 Pipeline 顺序示例 pipeline.addLast(new LineBasedFrameDecoder(1024)); // ① 先按行切包(限长 1024 防攻击) pipeline.addLast(new StringDecoder()); // ② 字节转字符串 pipeline.addLast(new MyServerHandler()); // ③ 业务处理
8ByteBuf 速记
比 NIO 的 ByteBuffer 好用在哪?看一眼就懂。
- 两个指针:
readerIndex(读到哪)、writerIndex(写到哪)。读写互不影响,不用 flip()。 - 自动扩容:写超了容量它会自己长大,不像 ByteBuffer 要手动 allocate。
- 池化 + 引用计数:高性能,但也意味着用完了要释放(下一节讲)。
9新手最常踩的 4 个坑
看完这节,你能避开 80% 的 Netty 线上事故。
数据库查询、HTTP 调用、sleep 都算。这会卡死整个 Worker 线程,同线程的所有连接都跟着卡。
✅ 解决:把耗时任务交给独立的业务线程池(
EventExecutorGroup 或自己 Executor),Handler 里只做 I/O。
自己
alloc().buffer() 出来的 ByteBuf 用完后要 release();在 channelRead 里如果不继续往下传也要释放。✅ 解决:用
SimpleChannelInboundHandler 代替 ChannelInboundHandlerAdapter,它会在读完自动释放。
没加 FrameDecoder 就直接按消息解析,数据一多就乱套(见第 7 节)。
异常不处理,连接不会正常关闭,时间长了一堆"僵尸连接"占着资源。
✅ 解决:至少打日志 +
ctx.close()。
10给你的学习路径建议
别一上来就啃源码,按这个顺序最稳。
- 先跑通第 5、6 节的 Echo 例子,建立"连接 → Pipeline → Handler"的直觉。
- 搞懂线程模型(第 4 节)和 6 个核心概念(第 3 节)——这是骨架。
- 吃透编解码(第 7 节),写一个"自定义协议"(长度 + 内容)的服务端。
- 练习心跳机制:
IdleStateHandler检测空闲连接并关闭。 - 再深入:零拷贝(
FileRegion/CompositeByteBuf)、内存池、源码里的 Reactor 实现。
11Epoll 是什么?(Linux 高性能的秘密)
你在第 4 节看到的"少量线程管海量连接",底层靠的就是它。
Epoll 是 Linux 内核提供的 I/O 多路复用机制,让一个线程能同时监听成千上万个连接(文件描述符 fd),谁"可读/可写"了就通知你去处理。它是 select / poll 的升级版。
- select / poll:每次调用都要把"全部 fd 集合"从用户态拷进内核,内核遍历所有 fd找就绪的 → 复杂度 O(N),连接越多越慢。
- epoll:fd 只注册一次,内核通过回调把就绪的 fd 放进"就绪链表",
epoll_wait直接拿就绪列表 → 复杂度 O(1),与总连接数无关。
NioEventLoopGroup 底层用 Java NIO 的 Selector,Linux 上其实就是 epoll;想更极致可用 Netty 原生的 EpollEventLoopGroup(额外支持 SO_REUSEPORT)。换平台 Netty 自动切换,业务代码不动。12TCP_NODELAY:实时服务为何要关掉 Nagle
TCP_NODELAY = true 禁用 Nagle 算法,让小数据立即发出,降低延迟。
Nagle 算法(默认开启)的初衷是省带宽:应用频繁写小数据时,OS 不会马上发,而是暂存起来,等攒够一个 MSS 或收到对端 ACK 才发,从而把多个小包合并成一个大包。
.childOption(ChannelOption.TCP_NODELAY, true)(注意是 childOption,对每条连接生效,不是 option)。实时 / 交互型服务几乎无脑加这句;仅"大文件传输、批量吞吐优先"才保留 Nagle。13绑定端口到底干了什么?
概念上 bind 和 listen 是两步,但在 Netty / Java 里被合成一步了。
一部服务端套接字要经历四个阶段:socket() → bind() → listen() → accept()。
- bind(端口):把套接字和某个本地端口 + IP 关联,端口被本进程「占住」(别人一般绑不了同一个)。
- listen():进入被动监听态,内核为它维护"待处理连接队列"(backlog),开始接客。
- accept():从队列取出已完成三次握手的连接,交给 Worker(对应第 4 节 Boss → Worker)。
ServerSocketChannel 本身就是"监听型"套接字,bind() 完自动进入监听态;Netty 的 bind(8080) 一步把 bind + listen 都做了,sync() 返回即已在 8080 监听、可接收连接。你无法、也不用再单独调 listen()。bind(0) 让 OS 随机挑空闲端口;backlog 可用 .option(ChannelOption.SO_BACKLOG, 128) 设置,控制瞬间大量连接时内核能排多少。


