• 传统 IO 的 Stream 和 NIO 的 Channel 的区别
① 单向 vs 双向 (Directionality)
  • Stream(流)是单向的
    • 如果你想读文件,必须创建 FileInputStream
    • 如果你想写文件,必须创建 FileOutputStream
    • 水流只能朝一个方向流动。
  • Channel(通道)是双向的
    • 一个 FileChannel 既可以用来读(read()),也可以用来写(write())。
    • 它更像是一条双向车道,或者铁路。
② 阻塞 vs 非阻塞 (Blocking vs Non-blocking)
  • Stream 是阻塞的
    • 当一个线程调用 read()write() 时,该线程被阻塞,直到有数据可读,或数据完全写入。在此期间,线程不能干其他任何事情。
  • Channel 支持非阻塞(主要针对网络通道,如 SocketChannel):
    • 通道可以设置为非阻塞模式。当进行读写操作时,如果没有数据可用,它会立即返回(返回 0 或 null),而不会让线程一直等待。线程可以去干别的事情。
③ 面向流 vs 面向缓冲区 (Stream-oriented vs Buffer-oriented)
  • Stream 是面向流的
    • 传统 IO 每次从流中读取一个或多个字节,数据没有被缓存在任何地方。你不能在流中前后移动读取指针(除非使用带缓存的流,且非常受限)。
  • Channel 是面向缓冲区的
    • Channel 不直接与数据交互,它必须通过 Buffer
    • 读取数据:ChannelBuffer(线程从 Buffer 读)。
    • 写入数据:BufferChannel(线程往 Buffer 写)。
    • 因为有了 Buffer,你可以方便地在 Buffer 中前后移动指针,灵活度极高。
④ 多路复用 (Selectors)
  • Stream 无法做到多路复用。在网络编程中,通常一个客户端连接(Socket)就需要一个独立的线程来维持。如果有 10000 个并发连接,就需要 10000 个线程,系统开销极大。

Channel 可以注册到 Selector(选择器)上。一个线程可以通过 Selector 监听成千上万个 Channel 上的事件(如:连接、数据到达、可写等)。这使得 单线程管理数万个连接 成为可能,是高并发网络框架(如 Netty)的核心基础。

               +------------------+
               |     Thread       |  <--- 1个线程掌控全局
               +------------------+
                        |
                        v
               +------------------+
               |     Selector     |  <--- 轮询哪些通道有事件发生
               +------------------+
                 /      |      \
               /        |        \  (监听事件:OP_READ, OP_WRITE...)
             v          v          v
      +---------+  +---------+  +---------+
      | Channel |  | Channel |  | Channel |  <--- 多个非阻塞通道
      +---------+  +---------+  +---------+
           ^            ^            ^
           | 读写数据    | 读写数据    | 读写数据
           v            v            v
      +---------+  +---------+  +---------+
      | Buffer  |  | Buffer  |  | Buffer  |  <--- 数据必须通过 Buffer
      +---------+  +---------+  +---------+

使用场景

  • 使用 Stream(传统 IO)的场景
    • 对并发要求不高,代码追求简单易懂。
    • 进行简单的本地文件读写。
  • 使用 Channel(NIO)的场景
    • 需要构建 高并发、低延迟 的网络服务器(如 Web 服务器、RPC 框架、即时通讯系统)。
    • 需要传输超大文件(FileChannel 提供了 transferTo/transferFrom 方法,可以使用操作系统的 零拷贝/Zero-Copy 技术,性能极高)。

1. 痛点:JDK NIO 编程复杂度极高,极易出错

直接使用 JDK NIO 编写网络程序,你需要处理大量底层的细节:

  • Buffer 的繁琐操作:JDK 的 ByteBuffer 只有一个位置指针(position),读写转换时必须手动调用 flip()clear()。这种设计极易出错,一旦忘记 flip() 就会导致数据错乱。
  • 网络协议的处理:TCP 协议是面向字节流的,存在粘包和拆包问题。在 JDK NIO 中,你需要自己写代码去处理半包、粘包,计算包长度,这非常考验程序员的功底。
  • 复杂的异常处理:连接断开、重连、网络闪断、半死连接、I/O 线程阻塞等,都需要自己写大量的防御性代码。

    Netty 的解决方案:
  • 提供了优雅的 ChannelPipeline 和 ChannelHandler 责任链模式,将网络事件(读、写、连接、断开)和业务逻辑解耦。
  • 提供了开箱即用的拆包器/粘包器(如 LengthFieldBasedFrameDecoder),几行代码就能搞定复杂的协议解析

2. 致命伤:JDK NIO 著名的 Epoll Bug(空轮询导致 CPU 100%)

这是 JDK 在 Linux 平台上的一个著名 Bug(JDK-6403933):
在 Linux 环境下,即使没有感兴趣的事件发生,Selector.select() 方法也有可能被意外唤醒,从而导致 while(true) 循环不断执行,CPU 占用率瞬间飙升到 100%。这个 Bug 存在了很久,虽然 JDK 尝试过修复,但在某些特定版本的 Linux 内核上依然会发生。

Netty 的解决方案:
Netty 在底层对这个 Bug 进行了规避和重建。Netty 会检测 select() 操作的执行频率,如果发现某个 Selector 在短时间内空轮询了 N 次(默认 512 次),Netty 就会判定触发了该 Bug。此时,Netty 会 重建 Selector,将旧 Selector 上的 Channel 重新注册到新 Selector 上,从而完美解决了这个让无数开发者头疼的 Bug。


3. 性能压榨:Netty 极致的内存与零拷贝优化

在高性能场景下,垃圾回收(GC)和内存复制是最大的性能杀手。JDK NIO 在这方面支持有限,而 Netty 做了大量的极致优化:


4. 线程模型:开箱即用的 Reactor 模式

编写高性能网络服务器,合理的线程模型是关键。著名的 Reactor 线程模型(单 Reactor 单线程、单 Reactor 多线程、主从 Reactor 多线程)是公认的佳作。


5. 协议支持与生态繁荣

总结

对于 Dubbo 和 RocketMQ 这类中间件来说,网络通信是它们的基石,但不是它们的业务核心

因此,不重复造轮子,选择业内事实上的标准(Netty),是这些优秀开源项目最理性的选择。

NIO 和 Netty 比较

  • 痛点:JDK NIO 编程复杂度极高,极易出错

    直接使用 JDK NIO 编写网络程序,你需要处理大量底层的细节:

  • Buffer 的繁琐操作:JDK 的 ByteBuffer 只有一个位置指针(position),读写转换时必须手动调用 flip()clear()。这种设计极易出错,一旦忘记 flip() 就会导致数据错乱。
  • 网络协议的处理:TCP 协议是面向字节流的,存在 粘包和拆包 问题。在 JDK NIO 中,你需要自己写代码去处理半包、粘包,计算包长度,这非常考验程序员的功底。
  • 复杂的异常处理:连接断开、重连、网络闪断、半死连接、I/O 线程阻塞等,都需要自己写大量的防御性代码。
  • 提供了开箱即用的 拆包器/粘包器(如 LengthFieldBasedFrameDecoder),几行代码就能搞定复杂的协议解析。
  • ByteBuf 替代 ByteBuffer:Netty 自研的 ByteBuf 拥有读写双指针(readerIndexwriterIndex),读写无需 flip(),API 更加人性化。
  • 内存池(PooledByteBufAllocator):Netty 引入了类似于 Jemalloc 的内存池技术。重用 ByteBuf 内存,极大地减少了频繁创建和销毁内存带来的 GC 压力。
  • 零拷贝(Zero-Copy)支持
    • 支持 CompositeByteBuf,可以将多个 Buffer 组合成一个逻辑 Buffer,避免了内存拷贝。
    • 封装了 FileChannel.transferTo(),可以直接将文件数据从内核缓冲区发送到网卡,不经过用户态。
  • 如果用 JDK NIO,你需要自己写大量的多线程代码去调度 Selector、Acceptor 和 Worker 线程,保证线程安全,防止死锁,门槛极高。
  • Netty 完美实现了 主从 Reactor 多线程模型。你只需要创建两个 EventLoopGroup(通常叫 bossGroupworkerGroup),一个负责接收连接,一个负责处理读写,几行代码就配置好了业界最优秀的线程模型。
  • 协议支持:直接用 JDK NIO,如果要实现 HTTP、WebSocket、SSL/TLS 加密传输,你需要自己写成千上万行的解析代码。而 Netty 已经内置了这些协议的支持,只需要在 Pipeline 中添加相应的 Handler 即可(如 HttpServerCodecSslHandler)。
  • 社区与验证:Netty 经历了十多年的全球高并发场景洗礼。苹果、微博、阿里、腾讯、美团等大厂都在大规模使用。它的健壮性、稳定性和文档完善度是任何个人或单一团队自己封装 NIO 无法比拟的。
  • 如果直接用 JDK NIO:这些团队需要组建专门的专家小组,花费几个月甚至更久去解决 Epoll Bug、内存泄漏、粘包拆包、多线程调度等底层网络细节,这严重分散了开发业务功能(如 RPC 路由、消息存储、消费队列)的精力。
  • 选择 Netty:相当于直接站在了巨人的肩膀上,获得了一个经过全球顶级流量验证、零 Bug(已被规避)、极高性能、极其稳定的网络通信底座。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐