Spring boot 4 探究netty的关键知识点

文章目录

Netty 是一款基于 NIO 的高性能异步事件驱动网络框架。本篇博客梳理了其核心组件: Zero CopyEventLoopHeartBeat 等关键知识点。掌握这些关键知识点,能助你构建高并发、低延迟的网络应用,轻松应对粘包拆包与心跳检测等常见问题。

netty关键知识讲解

Reactor 设计模式的实现

Netty 采用了主从多线程 Reactor 模型(Main-Sub Reactor) ,通过巧妙的组件分工,将"连接建立"和"I/O 读写"分离到不同的线程组中处理。

在 Netty 的 Reactor 实现中,EventLoopGroup 是基石:

  1. 分工明确 :通过将 EventLoopGroup 分为 BossWorker 两组,实现了"接收连接"与"处理 I/O"的分离,避免了相互阻塞。
  2. 屏蔽细节 :作为开发者,我们不需要关心底层是 NioEventLoop 还是 EpollEventLoop,我们只需要面向 EventLoopGroup 进行线程模型的配置。
  3. 可扩展性 :这种模型允许我们根据业务需求灵活调整线程组的大小,甚至可以自定义 ThreadFactory 来控制线程的创建方式(如设置线程名、优先级等)。

Netty Reactor 核心架构与角色映射表

  • Bootstrap / ServerBootstrap:客户端和服务端的启动辅助类。
  • ChannelPipeline :责任链模式,存放 ChannelHandler 的容器,数据在其中流动。
  • ChannelFuture :Netty 是异步的,ChannelFuture 用来监听异步操作的结果(如连接是否成功、写入是否完成)。通常通过 addListener 添加监听器,而不是阻塞等待 sync()
Reactor 模式角色 Netty 对应核心概念 职责与协作机制解析
Main Reactor(主反应器) EventLoopGroup** (Boss Group)** 连接管理者 。 • 负责监听服务端端口,处理客户端的连接接入事件(OP_ACCEPT)。 • 通常只需少量线程(甚至1个),因为它只负责建立连接,不处理复杂的 I/O 读写。 • 在代码中,它作为 ServerBootstrapgroup 参数传入。
Sub Reactor(从反应器) EventLoopGroup** (Worker Group)** I/O 读写处理器 。 • 负责处理已建立连接的网络 I/O 事件(OP_READ/OP_WRITE)。 • 通常包含多个线程(默认 CPU 核心数 × 2),以充分利用多核处理高并发。 • 在代码中,它作为 ServerBootstrapworkerGroup 参数传入。
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

EpollEventLoopGroupNioEventLoopGroup 是 Netty 中用于处理 I/O 事件的EventLoopGroup实现。

  • NioEventLoopGroup 是标准的 Java NIO 实现,具有很好的跨平台性,但在处理海量连接时,受限于 JVM 的封装,会有一定的性能损耗和 GC 压力。

  • EpollEventLoopGroup 是 Netty 针对 Linux 系统提供的本地传输实现。它通过 JNI 调用直接使用 Linux 的 epoll 机制。相比 NIO,它主要有三个优势:

    1. 性能更强epoll 采用边缘触发模式,且不需要像 select 那样轮询所有连接,适合高并发场景。
    2. 资源更省:减少了 Java 堆内存和堆外内存之间的拷贝,产生的垃圾对象少,降低了 Full GC 的风险。
    3. 功能更细 :支持配置 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 的三个参数

  1. readerIdleTime (读空闲) :如果在指定时间内没有读取 到数据,触发 READER_IDLE
  2. writerIdleTime (写空闲) :如果在指定时间内没有写入 数据,触发 WRITER_IDLE
  3. 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()(如果是堆外内存)。
    • ChannelInboundHandlerchannelRead 方法中
      • 如果你对消息进行了替换(例如解码成了对象),Netty 会自动释放旧的 ByteBuf
      • 如果你没有替换(例如只是读取了数据),你需要手动释放,或者在 ChannelOption 中配置 AUTO_READ

SelectorProvider的实现

EventLoopGroupSelectorProviderSelector 的源码实现,是 Netty 底层高性能和跨平台能力的核心所在。

构造流程图:

  1. 获取 ProviderNioEventLoopGroup 初始化时,确定使用哪个 SelectorProvider(NIO/E Poll 等)。
  2. 传递 ProviderEventLoopGroup 在创建 NioEventLoop 线程时,将 provider 作为构造参数传入。
  3. 创建 SelectorNioEventLoop 调用 provider.openSelector() 得到一个原生的 Selector 实例。
  4. 注入优化 :Netty 利用反射,将 Selector 内部的 selectedKeys 替换为数组结构,提升事件轮询的遍历性能

Netty 为什么要自己维护 SelectorProvider?

  1. 解耦:通过 Provider 模式,Netty 可以在不修改上层代码的情况下,替换底层的 I/O 模型(例如从 NIO 切换到 Epoll)。
  2. 优化:拿到 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 的工作模式通常遵循以下步骤:

  1. 创建与打开 :通过 Selector.open() 获取一个选择器实例。
  2. 注册通道 :将 Channel(如 SocketChannel)注册到 Selector 上,并指定你感兴趣的事件(例如 SelectionKey.OP_READ)。
    • 注意:通道必须配置为非阻塞模式(configureBlocking(false))才能注册到 Selector。
  3. 轮询就绪事件 :调用 selector.select() 方法。这个方法会阻塞,直到至少有一个通道注册的事件就绪。
  4. 处理事件 :一旦 select() 返回,说明有事件发生了。通过 selector.selectedKeys() 获取到就绪的 SelectionKey 集合,遍历这些 Key,根据事件类型(isAcceptable(), isReadable() 等)进行相应的处理。

TCP 参数调优

Netty 应用层核心参数 (ChannelOption)

在 Netty 的 BootstrapServerBootstrap 中,我们通过 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 filesCannot 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_rmemtcp_wmem 的最大值,以提升吞吐量(利用 BDP 原理)。

如何优化 Netty 以支持百万长连接?

  • 系统层调优 :Linux 系统层面要修改最大文件句柄数(ulimitfs.file-max)、调整 net.core.somaxconn 保证连接队列不溢出
  • Netty 参数调优
    • 在代码层面,SO_BACKLOG 要设大,保证能接住握手风暴
    • 内存控制SO_RCVBUFSO_SNDBUF 不能设太大,要根据业务消息体的平均大小来定,或者使用 Netty 的动态分配器,防止百万连接把堆外内存撑爆
  • 资源回收
    • 设置合理的 TCP_KEEPALIVE 和应用层心跳,及时剔除死连接,防止句柄耗尽。
    • JVM 参数要配合,避免频繁 Full GC 导致线程停顿,引发 TCP 重传。

设计模式

netty 的代码结构非常优雅,通过灵活运用多种设计模式,实现了高内聚、低耦合以及极佳的扩展性。

  • Reactor: 借鉴了 Doug Lea 的《Scalable IO in Java》中的 Reactor 模式,实现了主从多线程模型。
  • 责任链模式ChannelPipelineChannelHandler
    • 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 通道

关键角色

  1. Reactor(反应器):事件循环的核心。它负责调用 I/O 多路复用接口(Demultiplexer)监听事件,并在事件就绪后将其分发给对应的处理器。
  2. Demultiplexer(分发器/多路复用器) :系统级的事件监听器(如 epoll_wait),负责将多个 I/O 事件的等待合并为一次系统调用,避免线程阻塞。
  3. 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 等底层机制时,采用边缘触发模式可以减少事件被重复通知的次数,提高效率,但编程复杂度会相应增加(需要一次性处理完所有数据)
相关推荐
一 乐1 天前
餐厅点餐|基于springboot + vue餐厅点餐系统(源码+数据库+文档)
java·前端·数据库·vue.js·spring boot·后端
用户93816912553601 天前
Head First 单例模式
后端·设计模式
zs宝来了1 天前
大厂面试实录:Spring Boot源码深度解析+Redis缓存架构+RAG智能检索,谢飞机的AI电商面试之旅
spring boot·redis·微服务·大厂面试·java面试·rag·spring ai
小猪配偶儿_oaken1 天前
SpringBoot实现单号生成功能(Java&若依)
java·spring boot·okhttp
KawYang1 天前
Spring Boot 使用 PropertiesLauncher + loader.path 实现外部 Jar 扩展启动
spring boot·后端·jar
用户8307196840821 天前
揭秘 Spring Boot 事务:动态增强的底层实现与核心组件
spring boot
计算机程序设计小李同学1 天前
汽车4S店管理系统设计与实现
前端·spring boot·学习
a3535413821 天前
设计模式-桥接模式
c++·设计模式·桥接模式
sxlishaobin1 天前
设计模式之外观模式
java·设计模式·外观模式