Netty 深入学习
在分布式系统、微服务架构中,网络通信是最基础也是最重要的一部分。
很多高性能框架(如 Dubbo、gRPC、RocketMQ、Elasticsearch 等)底层都依赖 Netty 来完成网络通信。
我最初接触 Netty 时,直接去看它的 API 和示例代码,结果越看越糊涂:为什么要有 EventLoop?为什么连接和读写要分开两组线程?后来才意识到,这些设计不是凭空出现的,而是为了解决传统网络编程的固有问题。绕过这段演进史去学 Netty,等于背答案不看题目。
所以这篇笔记不急着写代码,先把 Netty 背后的 NIO 网络模型设计思想 理清楚。理解了这条主线,Netty 的各种组件就都能对号入座了。
传统网络编程的问题
在早期 Java 网络编程中,大多数程序使用的是 BIO(Blocking IO) 模型。
BIO 的"阻塞"体现在两个地方:accept() 等待新连接时会阻塞,read() 等待数据到达时也会阻塞。一个线程一旦阻塞在某个连接的读写上,就什么别的事都干不了。
例如:
服务器每接入一个客户端连接,就创建一个线程。
一个连接 = 一个线程
这个模型的好处是编程简单直观——每个线程只管自己那一个连接,从头读到尾就行。连接数少的时候完全够用。
但如果连接很多,比如:
1万连接 = 1万个线程
这会带来几个严重问题:
线程资源消耗巨大
线程本身需要内存和调度成本。每个 Java 线程都要占用独立的栈内存,连接数上万时,光是线程栈就能吃掉大量内存,还没算上操作系统维护线程结构的开销。
线程上下文切换开销大
CPU需要频繁在不同线程之间切换。每次切换都要保存和恢复寄存器、刷新缓存,线程数远超 CPU 核心数时,相当一部分 CPU 时间花在了切换本身,而不是处理业务。
系统扩展性差
连接数量一多,系统就容易崩溃。更尴尬的是,大量连接其实是空闲的(比如长连接场景下多数客户端并不总在发数据),却各自占着一个线程干等——资源浪费在了"等待"上。
因此,传统 BIO 并不适合 高并发网络服务。
NIO 的核心设计思想
为了解决 BIO 的问题,Java 提出了 NIO(Non-Blocking IO)。
NIO 的核心思想非常简单:
用少量线程,管理大量连接
思路的转变在于:既然大部分连接大部分时间都是空闲的,那就不要让线程守着连接干等,而是把"哪个连接有数据了"这件事交给操作系统去监视,线程只在真正有事件发生时才出手处理。这就是 IO 多路复用 的思想,在 Linux 上对应的就是 select/poll/epoll 这类系统调用。
它依赖三个核心组件:
Channel
Buffer
Selector
这三者构成了 NIO 的核心架构。下面逐个来看。
Channel(通道)
Channel 可以理解为:
数据传输的管道
和传统 IO 不同的是:
传统 IO:
输入流 / 输出流
而 Channel:
是双向的
既可以读,也可以写。Channel 本质就是 网络连接的抽象。
在 Java NIO 中,常用的实现有 ServerSocketChannel(监听端口、接收连接)和 SocketChannel(代表一条具体的 TCP 连接)。Channel 可以被设置为非阻塞模式——这是它能被 Selector 统一管理的前提:读不到数据时立即返回,而不是卡住线程。
Buffer(缓冲区)
在 NIO 中,所有数据都必须先进入 Buffer。
可以理解为:
数据的临时存储区域
数据流程:
网络 -> Buffer -> 程序
程序 -> Buffer -> 网络
Buffer 本质是一块带状态的内存区域,内部靠 position、limit、capacity 几个指针来记录"写到哪了、能读到哪"。读写模式的切换要靠 flip() 这类方法完成,这也是 NIO 原生 API 出了名容易写错的地方之一。Netty 后来自己实现了 ByteBuf,很大程度上就是为了摆脱这套别扭的操作。
Selector(选择器)
Selector 是 NIO 最核心的组件。
它的作用是:
用一个线程管理多个 Channel
也就是:
一个线程
监听多个连接
工作方式类似:
事件轮询
具体来说,每个 Channel 注册到 Selector 时会声明自己关心的事件类型:新连接到达(ACCEPT)、数据可读(READ)、可以写出(WRITE)等。线程调用 select() 阻塞等待,一旦有任意 Channel 就绪,就返回就绪的集合,线程挨个处理即可。
流程:
Selector
|
监听多个 Channel
|
哪个 Channel 有事件
|
处理哪个
这样一来,线程的时间全部花在"处理就绪事件"上,而不是"等待某个连接"上——这正是 NIO 相比 BIO 的根本差别。
Reactor 线程模型
Reactor 线程模型是一种 高并发网络服务器常用的设计模式。它的核心思想是:用少量线程,通过事件驱动的方式去处理大量网络连接。线程不再为每一个连接单独创建,而是通过监听网络事件(连接、读、写等),当某个连接有数据到达时再去处理它。这样就避免了大量线程带来的资源消耗和上下文切换问题,大大提升了服务器的并发处理能力。

Reactor 模型通常包含几个关键角色:
1、事件监听(Reactor)
负责监听网络事件,例如新的连接到来、数据可读、数据可写等。
2、连接接入(Acceptor)
当有新的客户端连接时,负责接收连接并注册到后续的处理线程中。
3、事件分发(Dispatcher)
将不同的网络事件分发给对应的处理逻辑。
4、业务处理(Handler)
真正执行业务逻辑,比如解析协议、处理请求、返回结果。
Reactor 模型本身也有几种演进形态:单 Reactor 单线程(一个线程包揽监听和处理)、单 Reactor 多线程(监听单线程、处理交给线程池)、主从 Reactor(连接接入和读写处理分别由不同的 Reactor 线程组负责)。连接量越大,越需要把接入和读写拆开,避免相互拖累。
在 Netty 中,Reactor 模型通常体现为 BossGroup + WorkerGroup 的线程结构,对应的正是主从 Reactor 形态:
- Boss 线程:负责接收客户端连接
- Worker 线程:负责处理网络读写和业务逻辑
通过这种设计,Netty 可以用 少量线程处理成千上万的连接,这也是它能够实现高性能网络通信的核心原因。
踩坑与注意
学习和使用这套模型时,有几点值得提前知道:
1)不要在 Worker 线程里做耗时操作。Reactor 模型的前提是事件处理足够快,一个 Worker 线程通常服务多个连接,如果 Handler 里做了慢查询、同步远程调用,这个线程上的其他连接会被一起拖住。耗时业务应该丢给独立的业务线程池。
2)直接用原生 NIO API 很容易写错。Buffer 的读写切换、Selector 的事件处理细节都有不少坑,这也是实际项目几乎都选择 Netty 而不是裸写 NIO 的原因——它把这些复杂性封装掉了。
3)NIO 快在"省线程",不在单次 IO。对于连接数很少的场景,BIO 未必更差;NIO/Reactor 的优势是在大量连接并存时体现出来的。技术选型要看场景。
小结
回顾这条演进主线:
- BIO 的问题是"一连接一线程",线程被空闲连接白白占用;
- NIO 用 Channel + Buffer + Selector 实现 IO 多路复用,让少量线程只处理就绪事件;
- Reactor 模型在此之上定义了事件监听、分发、处理的职责划分;
- Netty 的 BossGroup + WorkerGroup 就是主从 Reactor 的工程化实现。
理解了这条线,再去看 Netty 的 EventLoop、Pipeline、ByteBuf 这些具体组件,就不会觉得它们是凭空冒出来的设计了。后续再单独展开 Netty 的核心组件与实战用法。
评论 / COMMENTS