文章目录
- netty关键知识讲解
-
- [Reactor 设计模式的实现](#Reactor 设计模式的实现)
-
- [Netty Reactor 核心架构与角色映射表](#Netty Reactor 核心架构与角色映射表)
- 事件循环组EventLoopGroup
- 零拷贝
- [TCP粘包/拆包 及解码器](#TCP粘包/拆包 及解码器)
- 心跳
-
- [IdleStateHandler 的三个参数](#IdleStateHandler 的三个参数)
- 如何防止网络抖动误判?
- [IdleStateHandler 放在 Pipeline 的什么位置?](#IdleStateHandler 放在 Pipeline 的什么位置?)
- 汇总
- 异常处理与优雅停机
- `SelectorProvider`的实现
-
- [Netty 为什么要自己维护 SelectorProvider?](#Netty 为什么要自己维护 SelectorProvider?)
- 两种EventLoopGroup实现对比
- Selector
- [TCP 参数调优](#TCP 参数调优)
-
- [Netty 应用层核心参数 (ChannelOption)](#Netty 应用层核心参数 (ChannelOption))
- 操作系统层内核参数 (Sysctl)
-
- [A. 文件句柄数限制 (解决 "Too many open files")](#A. 文件句柄数限制 (解决 "Too many open files"))
- [B. 端口复用与回收 (解决 "Cannot assign requested address")](#B. 端口复用与回收 (解决 "Cannot assign requested address"))
- [C. 缓冲区与内存管理](#C. 缓冲区与内存管理)
- [如何优化 Netty 以支持百万长连接?](#如何优化 Netty 以支持百万长连接?)
- 设计模式
- 附录
-
- [基于Java Nio的server/client实现](#基于Java Nio的server/client实现)
- Reactor设计模式
Netty 是一款基于 NIO 的高性能异步事件驱动网络框架。本篇博客梳理了其核心组件: Zero Copy 、 EventLoop 与 HeartBeat 等关键知识点。掌握这些关键知识点,能助你构建高并发、低延迟的网络应用,轻松应对粘包拆包与心跳检测等常见问题。
netty关键知识讲解
Reactor 设计模式的实现
Netty 采用了主从多线程 Reactor 模型(Main-Sub Reactor) ,通过巧妙的组件分工,将"连接建立"和"I/O 读写"分离到不同的线程组中处理。
在 Netty 的 Reactor 实现中,EventLoopGroup 是基石:
- 分工明确 :通过将
EventLoopGroup分为 Boss 和 Worker 两组,实现了"接收连接"与"处理 I/O"的分离,避免了相互阻塞。 - 屏蔽细节 :作为开发者,我们不需要关心底层是
NioEventLoop还是EpollEventLoop,我们只需要面向EventLoopGroup进行线程模型的配置。 - 可扩展性 :这种模型允许我们根据业务需求灵活调整线程组的大小,甚至可以自定义
ThreadFactory来控制线程的创建方式(如设置线程名、优先级等)。
Netty Reactor 核心架构与角色映射表
- Bootstrap / ServerBootstrap:客户端和服务端的启动辅助类。
- ChannelPipeline :责任链模式,存放
ChannelHandler的容器,数据在其中流动。 - ChannelFuture :Netty 是异步的,
ChannelFuture用来监听异步操作的结果(如连接是否成功、写入是否完成)。通常通过addListener添加监听器,而不是阻塞等待sync()。
| Reactor 模式角色 | Netty 对应核心概念 | 职责与协作机制解析 |
|---|---|---|
| Main Reactor(主反应器) | EventLoopGroup** (Boss Group)** |
连接管理者 。 • 负责监听服务端端口,处理客户端的连接接入事件(OP_ACCEPT)。 • 通常只需少量线程(甚至1个),因为它只负责建立连接,不处理复杂的 I/O 读写。 • 在代码中,它作为 ServerBootstrap 的 group 参数传入。 |
| Sub Reactor(从反应器) | EventLoopGroup** (Worker Group)** |
I/O 读写处理器 。 • 负责处理已建立连接的网络 I/O 事件(OP_READ/OP_WRITE)。 • 通常包含多个线程(默认 CPU 核心数 × 2),以充分利用多核处理高并发。 • 在代码中,它作为 ServerBootstrap 的 workerGroup 参数传入。 |
| Acceptor(连接接收器) | ServerBootstrapAcceptor |
连接分配器 。 是ServerBootstrap中的内部类 • 它是 Boss Group 和 Worker Group 之间的桥梁。 • 当 Boss Group 接收到新连接时,由 Acceptor 负责将该连接注册到 Worker Group 中的某个 EventLoop 上,实现连接的负载均衡分配。 |
| Event Demultiplexer(事件多路复用器) | Selector |
事件监听核心 。 • 由 EventLoopGroup 内部管理(通过 SelectorProvider 创建)。 • 基于 I/O 多路复用机制(如 select/poll/epoll),让单个线程能够同时监控多个连接的 I/O 事件,这是 Reactor 模式高性能的基础。 |
| Handler(事件处理器) | ChannelHandler |
业务逻辑执行单元 。 • 负责处理具体的业务逻辑(如编解码、数据计算)。 • 在 Netty 中,Handler 被组织成 ChannelPipeline(责任链模式),数据在 Pipeline 中流动并被依次处理。 |
事件循环组EventLoopGroup
EpollEventLoopGroup 和 NioEventLoopGroup 是 Netty 中用于处理 I/O 事件的EventLoopGroup实现。
-
NioEventLoopGroup 是标准的 Java NIO 实现,具有很好的跨平台性,但在处理海量连接时,受限于 JVM 的封装,会有一定的性能损耗和 GC 压力。
-
EpollEventLoopGroup 是 Netty 针对 Linux 系统提供的本地传输实现。它通过 JNI 调用直接使用 Linux 的
epoll机制。相比 NIO,它主要有三个优势:- 性能更强 :
epoll采用边缘触发模式,且不需要像select那样轮询所有连接,适合高并发场景。 - 资源更省:减少了 Java 堆内存和堆外内存之间的拷贝,产生的垃圾对象少,降低了 Full GC 的风险。
- 功能更细 :支持配置 Linux 特有的 TCP 参数,如
TCP_CORK。
- 性能更强 :
很多优秀的开源框架(如 RocketMQ、Dubbo)在启动时都会检测当前操作系统,如果是 Linux 则自动使用 Epoll,否则回退到 NIO,以此来保证兼容性和性能的最优平衡
| 维度 | NioEventLoopGroup | EpollEventLoopGroup |
|---|---|---|
| 底层机制 | Java NIO (Select/Poll/Epoll) | Linux Native (JNI 调用 epoll) |
| 触发模式 | 水平触发 | 边缘触发 |
| 运行平台 | 跨平台 (Windows/Mac/Linux) | 仅支持 Linux |
| 性能与 GC | 一般,相对较多 GC | 更高,更少 GC |
| 配置参数 | 标准 TCP 参数 | 支持更多 Linux 特有参数 (如 TCP_CORK) |
零拷贝
- Netty 的"零拷贝"是如何实现的?
- **零拷贝不仅指操作系统层面的,还包括 Netty 应用层面的优化。
- CompositeByteBuf:将多个 ByteBuf 聚合成一个逻辑上的 ByteBuf,避免了合并时的内存复制。
- FileRegion :使用
FileChannel.transferTo,在文件传输时减少用户态和内核态之间的上下文切换和数据拷贝。 - 堆外内存:使用 DirectBuffer 避免 JVM 堆内存和堆外内存之间的数据拷贝。
- 内存池化 :Netty 使用
PooledByteBufAllocator来复用内存,减少 GC 压力。
TCP粘包/拆包 及解码器
什么是 TCP 粘包/拆包?产生的原因是什么?
TCP 是面向流的协议,没有消息边界。发送方的多次写入可能被合并(粘包),或者一次写入被拆分成多次传输(拆包)
Netty 使用解码器解决 TCP 粘包和拆包 问题
解码器的适用场景 和工作原理有显著区别。
- FixedLengthFrameDecoder (固定长度解码器)
- DelimiterBasedFrameDecoder (分隔符解码器)
- LengthFieldBasedFrameDecoder (长度字段解码器)
- 最通用、最常用的方案。通过消息头中的一个字段(Length Field)来告知接收方"接下来我要发多少字节"
- 大多数二进制协议(如 Dubbo、gRPC、自定义 RPC 协议)的标准做法
- ReplayingDecoder (重放解码器)
- LineBasedFrameDecoder (行解码器)
- ProtobufDecoder
- JacksonJsonDecoder
- Jaxb2XmlDecoder
| 特性 | FixedLengthFrameDecoder | DelimiterBasedFrameDecoder | LengthFieldBasedFrameDecoder | ReplayingDecoder |
|---|---|---|---|---|
| 分类 | 具体解码策略 | 具体解码策略 | 具体解码策略 | 解码编程模型 |
| 核心依据 | 固定字节数 | 特殊分隔符 | 消息头中的长度字段 | 包装底层 Buffer |
| 灵活性 | 低 (定长) | 中 (变长文本) | 高 (通用二进制) | 高 (逻辑简洁) |
| 典型场景 | 简单指令 | 文本协议 (Telnet/Redis) | RPC、文件传输、私有协议 | 复杂协议解析 |
| 主要痛点 | 浪费带宽 | 内容不能含分隔符 | 协议格式定义较复杂 | 性能略低 (异常机制) |
心跳
TCP 既然已经有 KeepAlive,为什么还要在应用层做心跳?
- TCP KeepAlive 的缺陷:TCP 协议层的心跳默认超时时间通常很长(例如 2 小时),且依赖操作系统设置,不够灵活,无法满足应用层快速感知断线的需求。
- 连接假死 :网络可能出现"半包"或"僵死"连接(一端断电、网线拔掉)。此时 TCP 连接状态在代码里看还是
ESTABLISHED,但实际上已经不可用。如果不通过心跳检测,服务端会一直维护这些无效连接,消耗宝贵的内存和句柄资源。
为了及时发现并释放这些"假死"连接,减少服务端资源浪费,同时让客户端能及时感知连接状态并尝试重连。
IdleStateHandler 的三个参数
- readerIdleTime (读空闲) :如果在指定时间内没有读取 到数据,触发
READER_IDLE。 - writerIdleTime (写空闲) :如果在指定时间内没有写入 数据,触发
WRITER_IDLE。 - allIdleTime (读写空闲) :如果在指定时间内既没有读也没有写,触发
ALL_IDLE。
- 基于时间轮/定时任务 :
IdleStateHandler内部会启动一个定时任务(TimerTask),周期性地检查 Channel 的读写时间。- 时间更新机制 :
- 在
channelRead方法(数据读取后)会更新最后读取时间。 - 在
write方法(数据写出后)会更新最后写入时间。
- 在
- 触发逻辑 :定时任务运行时,会计算当前时间与"最后一次读/写时间"的差值。如果差值超过了设定的阈值,就会触发
userEventTriggered事件。
- 时间更新机制 :
- 注意点 :它是一个
ChannelHandler,必须添加到Pipeline中才能生效
如何防止网络抖动误判?
网络偶尔卡顿一下就断开连接,这在生产环境是不可接受的,你怎么解决?
- 不能一检测到空闲就立刻断开。
- 在
userEventTriggered中,定义一个计数器变量(如idleCount)。 - 每次触发
READER_IDLE,计数器加 1。 - 只有当计数器大于某个阈值(例如 3 次)时,才判定为真正的连接失效,执行
close()操作。 - 一旦收到正常业务消息,立即将计数器重置为 0。
IdleStateHandler 放在 Pipeline 的什么位置?
通常放在 最前面(靠近网络传输层)
如果放在业务 Handler 后面,数据必须先经过业务逻辑解码才能被 IdleStateHandler 检测到。如果客户端发的是非法包或者连接已经半死,业务解码可能会失败,导致 IdleStateHandler 无法正确统计时间。放在最前面能确保只要收到网络数据包(无论是否合法),都能刷新"读取时间"
汇总
| 维度 | 关键词 | 核心回答点 |
|---|---|---|
| 作用 | 假死连接、资源回收 | 解决 TCP KeepAlive 不够灵敏的问题,及时清理无效连接。 |
| 参数 | 读空闲、写空闲、全空闲 | 区分三个时间阈值的含义,服务端通常关注读空闲。 |
| 机制 | 定时任务、时间差值 | 内部通过定时扫描最后读写时间戳来判断是否空闲。 |
| 流程 | userEventTriggered | 必须配合自定义 Handler 重写该方法才能生效。 |
| 优化 | 抖动、计数器 | 不能一次超时就断连,需要累计几次超时才断开。 |
| 位置 | Pipeline 顺序 | 建议放在 Pipeline 链表的头部,避免业务逻辑干扰。 |
异常处理与优雅停机
- 如何处理空闲连接(IdleStateHandler)?
- 通常配合
userEventTriggered方法,检测到空闲状态后关闭连接。
- 通常配合
- Netty 如何实现优雅停机?
- 调用
EventLoopGroup.shutdownGracefully()。 - 原理 :不再接受新任务,等待已提交的任务执行完毕,最后释放线程资源。这比直接
System.exit()更安全,能保证正在传输的数据不丢失。
- 调用
- 常见的内存泄漏问题?
ByteBuf使用完后必须调用release()(如果是堆外内存)。- 在
ChannelInboundHandler的channelRead方法中- 如果你对消息进行了替换(例如解码成了对象),Netty 会自动释放旧的
ByteBuf; - 如果你没有替换(例如只是读取了数据),你需要手动释放,或者在
ChannelOption中配置AUTO_READ。
- 如果你对消息进行了替换(例如解码成了对象),Netty 会自动释放旧的
SelectorProvider的实现
EventLoopGroup 中 SelectorProvider 与 Selector 的源码实现,是 Netty 底层高性能和跨平台能力的核心所在。
构造流程图:
- 获取 Provider :
NioEventLoopGroup初始化时,确定使用哪个SelectorProvider(NIO/E Poll 等)。 - 传递 Provider :
EventLoopGroup在创建NioEventLoop线程时,将provider作为构造参数传入。 - 创建 Selector :
NioEventLoop调用provider.openSelector()得到一个原生的Selector实例。 - 注入优化 :Netty 利用反射,将
Selector内部的selectedKeys替换为数组结构,提升事件轮询的遍历性能。
Netty 为什么要自己维护 SelectorProvider?
- 解耦:通过 Provider 模式,Netty 可以在不修改上层代码的情况下,替换底层的 I/O 模型(例如从 NIO 切换到 Epoll)。
- 优化:拿到 Selector 实例后,Netty 会通过反射进行'无锁化'优化,将 JDK 默认的 HashSet 替换为数组,减少了哈希计算和锁竞争,这对高频的 I/O 事件轮询至关重要。"
两种EventLoopGroup实现对比
| 特性 | NioEventLoopGroup | EpollEventLoopGroup |
|---|---|---|
| 依赖的 Provider | java.nio.channels.spi.SelectorProvider |
无 (直接使用 JNI) |
| 创建 Selector 的方式 | provider.openSelector() |
Native.epollCreate() |
| 跨平台性 | 高 (JDK 原生支持) | 仅限 Linux (调用 Linux 系统 API) |
| 代码可见性 | 显式传入 SelectorProvider |
完全封装在 Epoll 类内部 |
| 在linux平台下使用EpollEventLoopGroup需要添加如下依赖 |
xml
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-transport-native-epoll</artifactId>
<version>4.2.9.Final</version>
</dependency>
Selector
在 Java NIO(New I/O)中,Selector(选择器) 是实现 I/O 多路复用(Multiplexing) 的核心组件。
主要作用
主要作用是:让一个单独的线程能够同时监控多个通道(Channel)的 I/O 事件(如连接接入、数据可读、数据可写等)
- 单线程管理多通道:一个线程只需要操作一个 Selector,就可以管理成百上千个 Channel。
- 事件驱动:Selector 会不断轮询它所注册的通道。只有当某个通道有 I/O 事件"就绪"(例如:客户端发来了数据,通道可读)时,Selector 才会将该事件通知给线程进行处理。
- 避免阻塞 :如果没有通道就绪,线程会阻塞在
select()方法上(或者通过配置超时返回),而不会像 BIO 那样在读写操作上死等,从而极大地提高了线程的利用率。
Selector 通过监听不同的事件来驱动程序逻辑。这些事件定义在 SelectionKey 中:
| 事件常量 | 作用描述 | 触发场景 |
|---|---|---|
OP_ACCEPT |
接收连接事件 | 服务端 ServerSocketChannel 监听到有新的客户端连接接入时。 |
OP_CONNECT |
连接建立事件 | 客户端 SocketChannel 完成与服务端的 TCP 三次握手时。 |
OP_READ |
数据可读事件 | 通道中有数据可供读取(例如客户端发来了消息)。 |
OP_WRITE |
数据可写事件 | 通道的写缓冲区有空闲,可以向通道写入数据时。 |
工作流程
Selector 的工作模式通常遵循以下步骤:
- 创建与打开 :通过
Selector.open()获取一个选择器实例。 - 注册通道 :将 Channel(如 SocketChannel)注册到 Selector 上,并指定你感兴趣的事件(例如
SelectionKey.OP_READ)。- 注意:通道必须配置为非阻塞模式(
configureBlocking(false))才能注册到 Selector。
- 注意:通道必须配置为非阻塞模式(
- 轮询就绪事件 :调用
selector.select()方法。这个方法会阻塞,直到至少有一个通道注册的事件就绪。 - 处理事件 :一旦
select()返回,说明有事件发生了。通过selector.selectedKeys()获取到就绪的SelectionKey集合,遍历这些 Key,根据事件类型(isAcceptable(),isReadable()等)进行相应的处理。
TCP 参数调优
Netty 应用层核心参数 (ChannelOption)
在 Netty 的 Bootstrap 或 ServerBootstrap 中,我们通过 option() 和 childOption() 设置这些参数。
| 参数名 | 作用 | 调优建议与面试话术 |
|---|---|---|
| SO_BACKLOG | 完成三次握手的连接队列长度。 | 场景:高并发瞬间涌入。 建议:默认值通常只有 128 或 1024。在海量连接场景下,必须调大(如 1024 或更高),否则新连接会被丢弃。 注意:它受限于操作系统 somaxconn,不能超过系统限制。 |
| SO_RCVBUF / SO_SNDBUF | TCP 接收/发送缓冲区大小。 | 误区:不要盲目设大。 建议:如果消息平均大小是 1KB,设成 64KB 就够了。如果设成 1MB,百万连接会吃掉几百 GB 内存。 Netty 特性:Netty 提供了 AdaptiveRecvByteBufAllocator(动态调整缓冲区大小),比固定大小更省内存。 |
| TCP_NODELAY | 禁用 Nagle 算法。 | 默认:Netty 默认开启(即 true,禁用 Nagle 算法)。 原因:Nagle 算法会把小包合并发送,虽然省带宽,但会增加延迟。对于实时性要求高的场景(如游戏、金融交易),必须禁用,保证有数据就发。 |
| SO_KEEPALIVE | TCP 层心跳机制。 | 作用:检测死连接(如客户端断电)。 局限:Linux 默认 2 小时才探测,太慢了。 结论:只能作为应用层心跳(Netty IdleStateHandler)的补充,不能替代。 |
| ALLOW_HALF_CLOSURE | 是否允许半关闭。 | 默认:Netty 默认是 false。 解释:TCP 是全双工的。如果设为 true,客户端发 FIN 关闭写端,服务端还能继续读。设为 false 会让连接在半关闭时立刻完全关闭,防止出现"僵死"连接占用资源。 |
操作系统层内核参数 (Sysctl)
当连接数达到几万甚至几十万时,如果不改系统参数,程序会报 Too many open files 或 Cannot assign requested address。这是面试的加分项,表明你有线上运维经验。
A. 文件句柄数限制 (解决 "Too many open files")
每个 TCP 连接在 Linux 中都是一个文件句柄。
- 参数 :
ulimit -n(用户级) /fs.file-max(系统级)。 - 作用:在百万连接测试前,必须将单进程打开文件数限制调至 100 万以上。否则新的 accept 会失败
B. 端口复用与回收 (解决 "Cannot assign requested address")
- net.ipv4.tcp_tw_reuse = 1
- 作用:允许将 TIME_WAIT 状态的 Socket 重新用于新的连接(仅客户端/连接发起方有效)。
- net.ipv4.tcp_tw_recycle = 0 (注意:在新内核中已废弃)
- 作用:快速回收 TIME_WAIT 连接。
- 警告:在 NAT 网络环境下(如用户通过路由器上网)可能导致连接失败,通常不建议开启。
- net.ipv4.ip_local_port_range = 1024 65535
- 作用:扩大本地端口范围,增加客户端可用的端口数量。
C. 缓冲区与内存管理
- net.core.somaxconn
- 这是
SO_BACKLOG的系统上限。如果应用层设了 1024,但系统somaxconn只有 128,最终生效的是 128。建议将其调大至 65535 或更高。
- 这是
- net.ipv4.tcp_mem / tcp_rmem / tcp_wmem
- 作用:控制 TCP 协议栈使用的内存大小。
- 调优 :在高带宽、高延迟的网络中(如跨机房传输),需要增大
tcp_rmem和tcp_wmem的最大值,以提升吞吐量(利用 BDP 原理)。
如何优化 Netty 以支持百万长连接?
- 系统层调优 :Linux 系统层面要修改最大文件句柄数(
ulimit和fs.file-max)、调整net.core.somaxconn保证连接队列不溢出 - Netty 参数调优 :
- 在代码层面,
SO_BACKLOG要设大,保证能接住握手风暴 - 内存控制 :
SO_RCVBUF和SO_SNDBUF不能设太大,要根据业务消息体的平均大小来定,或者使用 Netty 的动态分配器,防止百万连接把堆外内存撑爆
- 在代码层面,
- 资源回收 :
- 设置合理的
TCP_KEEPALIVE和应用层心跳,及时剔除死连接,防止句柄耗尽。 - JVM 参数要配合,避免频繁 Full GC 导致线程停顿,引发 TCP 重传。
- 设置合理的
设计模式
netty 的代码结构非常优雅,通过灵活运用多种设计模式,实现了高内聚、低耦合以及极佳的扩展性。
- Reactor: 借鉴了 Doug Lea 的《Scalable IO in Java》中的 Reactor 模式,实现了主从多线程模型。
- 责任链模式 :
ChannelPipeline和ChannelHandler。- ChannelPipeline 是一个双向链表,里面维护了多个 ChannelHandler。
- 当数据流入或流出时,会依次经过链路上的处理器。你可以随意在链表中添加(addLast)或删除处理器,而不需要修改其他处理器的代码
- 建造者模式【Builder Pattern】: ServerBootstrap 和 Bootstrap, 用于构建复杂的对象
- 工厂模式: 大量使用工厂模式来解耦对象的创建, 如ChannelFactory、ByteBufAllocator
- 适配器模式 :
ChannelInboundHandlerAdapter(提供了空实现,让你可以只重写感兴趣的方法)。 - 模板方法模式 :
ChannelHandler的各种实现类。 - 享元模式 :
@Sharable注解的 Handler,可以被多个 Pipeline 共享(前提是线程安全)。 - 适配器模式: ChannelInboundHandlerAdapter
- 装饰器模式: PooledSlicedByteBuf
- 代理模式: LocalChannel
- 观察者模式: ChannelFuture 和 ChannelFutureListener
- 策略模式:
- EventExecutorChooser:根据线程数选择下一个执行任务的 EventExecutor(轮询或位运算优化策略)。
- Decoder/Encoder:不同的编解码器(如 LengthFieldBasedFrameDecoder 和 StringEncoder)封装了不同的数据处理算法,可以互换使用。
- 模板方法模式: SimpleChannelInboundHandler 定义了处理流程的骨架(如 channelRead 中先判断消息类型,再调用 channelRead0),将具体的数据处理逻辑延迟到子类的 channelRead0 方法中实现。
附录
基于Java Nio的server/client实现
NioServerWithSelector
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.*;
import java.util.Iterator;
import java.util.Set;
public class NioServerWithSelector {
public static void main(String[] args) throws IOException {
// 1. 获取 Selector (多路复用器)
Selector selector = Selector.open();
// 2. 获取 ServerSocketChannel (代替 ServerSocket) ServerSocketChannel serverChannel = ServerSocketChannel.open();
// 3. 绑定端口
serverChannel.bind(new InetSocketAddress(8080));
// 4. 设置为非阻塞模式 (关键!否则无法注册到 Selector) serverChannel.configureBlocking(false);
// 5. 将 Channel 注册到 Selector 上,关注 "接收连接" 事件
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("NIO 服务端启动,监听 8080 端口...");
while (true) {
// 6. 阻塞等待,直到有通道就绪
// select() 返回值表示有多少个通道已经就绪
int readyChannels = selector.select();
if (readyChannels == 0) {
continue;
}
// 7. 获取就绪的事件集合 (SelectionKey 集合)
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
try {
// 8. 处理不同的就绪事件
if (key.isAcceptable()) {
// 有新的客户端连接进来 (相当于 ServerSocket.accept()) handleAccept(key, selector);
} else if (key.isReadable()) {
// 客户端发来了数据 (相当于 Socket.getInputStream().read()) handleRead(key);
} else if (key.isWritable()) {
// 通道可写 (通常用于发送大数据,避免缓冲区满)
handleWrite(key);
}
} catch (IOException e) {
e.printStackTrace();
// 取消该 key 的注册
key.cancel();
}
// 9. 必须手动移除当前处理过的 key,防止重复处理
keyIterator.remove();
}
}
}
// 处理连接事件
private static void handleAccept(SelectionKey key, Selector selector) throws IOException {
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
// 接受连接,获取 SocketChannel SocketChannel clientChannel = serverChannel.accept();
clientChannel.configureBlocking(false);
System.out.println("客户端已连接: " + clientChannel.getRemoteAddress());
// 将客户端通道注册到 Selector,关注 "读" 事件
clientChannel.register(selector, SelectionKey.OP_READ);
}
// 处理读事件
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
int read = clientChannel.read(buffer);
if (read > 0) {
buffer.flip(); // 切换为读模式
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String msg = new String(data);
System.out.println("收到消息: " + msg);
// 注册写事件,准备回写数据
// (实际生产中通常直接 write,这里为了演示 Selector 的写事件)
key.interestOps(SelectionKey.OP_WRITE);
} else if (read == -1) {
// 客户端断开连接
System.out.println("客户端断开: " + clientChannel.getRemoteAddress());
clientChannel.close();
key.cancel();
}
} catch (IOException e) {
// 客户端异常断开
System.out.println("客户端异常: " + clientChannel.getRemoteAddress());
clientChannel.close();
key.cancel();
}
}
// 处理写事件
private static void handleWrite(SelectionKey key) throws IOException {
SocketChannel clientChannel = (SocketChannel) key.channel();
String response = "HTTP/1.1 200 OK\r\nContent-Length: 12\r\n\r\nHello Netty!";
try {
clientChannel.write(ByteBuffer.wrap(response.getBytes()));
// 写完后,重新关注 "读" 事件,等待客户端下一次请求
key.interestOps(SelectionKey.OP_READ);
} catch (IOException e) {
clientChannel.close();
key.cancel();
}
}
}
NioClientWithSelector
java
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;
import java.util.Set;
public class NioClientWithSelector {
private static final String HOST = "127.0.0.1";
private static final int PORT = 8080;
public static void main(String[] args) throws IOException {
// 1. 打开 Selector 和 SocketChannel Selector selector = Selector.open();
SocketChannel channel = SocketChannel.open();
// 2. 设置为非阻塞模式
channel.configureBlocking(false);
// 3. 注册连接事件 (OP_CONNECT) // 注意:connect 是异步的,注册 OP_CONNECT 事件是为了等待连接建立完成
channel.register(selector, SelectionKey.OP_CONNECT);
// 4. 发起连接请求
channel.connect(new InetSocketAddress(HOST, PORT));
// 5. 轮询事件
while (true) {
selector.select(); // 阻塞直到有事件就绪
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> it = selectedKeys.iterator();
while (it.hasNext()) {
SelectionKey key = it.next();
it.remove(); // 必须移除,防止重复处理
try {
if (key.isConnectable()) {
// 处理连接就绪
handleConnect(key, selector);
} else if (key.isReadable()) {
// 处理服务端发来的消息
handleRead(key);
}
// 注意:通常不需要专门注册 OP_WRITE 来发送消息
// 因为 TCP 缓冲区通常总是可写的。只有在缓冲区满时才需要注册写事件。
// 这里我们通过控制台输入直接发送。
} catch (IOException e) {
e.printStackTrace();
key.cancel();
}
}
// 6. 检查控制台输入,发送消息给服务端
// 这里为了简单,直接在主线程检查输入,实际可以结合 Selector 或单独线程
if (channel.isConnected() && !channel.isBlocking()) {
Scanner scanner = new Scanner(System.in);
if (scanner.hasNext()) {
String line = scanner.nextLine();
// 直接写入(如果缓冲区满,可能会写不全,生产环境需处理)
channel.write(ByteBuffer.wrap(line.getBytes()));
System.out.println("已发送: " + line);
}
}
}
}
// 处理连接建立
private static void handleConnect(SelectionKey key, Selector selector) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
// finishConnect() 必须调用,以完成非阻塞连接的后续工作
if (channel.finishConnect()) {
System.out.println("成功连接到服务端");
// 连接建立后,取消 OP_CONNECT 监听,注册 OP_READ 监听服务端响应
channel.register(selector, SelectionKey.OP_READ);
} else {
System.err.println("连接失败");
System.exit(1);
}
}
// 处理读取服务端响应
private static void handleRead(SelectionKey key) throws IOException {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int read = channel.read(buffer);
if (read > 0) {
buffer.flip();
byte[] data = new byte[buffer.remaining()];
buffer.get(data);
String msg = new String(data);
System.out.println("收到服务端回复: " + msg);
} else if (read == -1) {
System.out.println("服务端断开连接");
channel.close();
key.cancel();
}
}
}
Reactor设计模式
Reactor 设计模式是高性能网络编程的基石,
Netty、Redis 等熟知的高性能中间件底层都离不开它。
Reactor 模式是一种事件驱动 的设计模式。它的核心思想是"非阻塞 I/O + 事件通知 ",
它通过一个事件循环(Event Loop) ,利用操作系统提供的 I/O 多路复用技术(如 Linux 的 epoll、BSD 的 kqueue),让单个线程能够同时监控多个 I/O 通道
关键角色
- Reactor(反应器):事件循环的核心。它负责调用 I/O 多路复用接口(Demultiplexer)监听事件,并在事件就绪后将其分发给对应的处理器。
- Demultiplexer(分发器/多路复用器) :系统级的事件监听器(如
epoll_wait),负责将多个 I/O 事件的等待合并为一次系统调用,避免线程阻塞。 - Handler(处理器):负责处理具体的 I/O 事件(如读取、写入数据)
主要形态
- 单 Reactor 单线程: 编程简单,没有锁竞争;但如果某个业务逻辑处理耗时过长(如复杂的计算),会阻塞整个事件循环,导致无法处理其他连接。
- 无锁开销
- 原子性保证
- 内存操作快
- 单 Reactor 多线程: I/O 操作和业务操作解耦,避免业务处理卡住 I/O 线程
- 主从 Reactor 多线程: 彻底的职责分离,Main Reactor 不会因为处理大量数据读写而卡顿,支持超大规模并发
| 维度 | 单 Reactor 单线程 | 单 Reactor 多线程 | 主从 Reactor 多线程 |
|---|---|---|---|
| 吞吐量 | 低(单线程瓶颈) | 中(依赖线程池大小) | 高(多 Reactor 并行) |
| 处理逻辑 | 所有操作(连接、读写、业务)都在同一线程 | Reactor 线程处理 I/O,业务逻辑交给线程池 | Main Reactor 处理连接,Sub Reactor 处理 I/O |
| 典型应用 | Redis(仅命令执行部分:单线程模型) | 传统即时通讯服务器 | Netty、Nginx、Kafka |
| 适用场景 | 低并发、轻量级服务 | 中等并发、业务较复杂 | 高并发、业务逻辑复杂 |
关键优化与避坑指南
- 避免阻塞 Reactor 线程: 一旦阻塞,整个事件循环就会停摆。务必把耗时任务提交到业务线程池
- 零拷贝(Zero-Copy): 避免了用户态和内核态之间的多次数据拷贝,极大提升了性能
- 内存池化 : 频繁创建和销毁缓冲区(Buffer)会产生大量垃圾对象。使用内存池(如 Netty 的
ByteBufAllocator)可以重用内存,减少 GC 压力 - 边缘触发(ET)模式 : 在使用
epoll等底层机制时,采用边缘触发模式可以减少事件被重复通知的次数,提高效率,但编程复杂度会相应增加(需要一次性处理完所有数据)