传统 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。读取数据Channel→Buffer线程从 Buffer 读。写入数据Buffer→Channel线程往 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的场景对并发要求不高代码追求简单易懂。进行简单的本地文件读写。使用 ChannelNIO的场景需要构建高并发、低延迟的网络服务器如 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 平台上的一个著名 BugJDK-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 替代 ByteBufferNetty 自研的ByteBuf拥有读写双指针readerIndex和writerIndex读写无需flip()API 更加人性化。内存池PooledByteBufAllocatorNetty 引入了类似于 Jemalloc 的内存池技术。重用ByteBuf内存极大地减少了频繁创建和销毁内存带来的 GC 压力。零拷贝Zero-Copy支持支持CompositeByteBuf可以将多个 Buffer 组合成一个逻辑 Buffer避免了内存拷贝。封装了FileChannel.transferTo()可以直接将文件数据从内核缓冲区发送到网卡不经过用户态。如果用JDK NIO你需要自己写大量的多线程代码去调度 Selector、Acceptor 和 Worker 线程保证线程安全防止死锁门槛极高。Netty完美实现了主从 Reactor 多线程模型。你只需要创建两个EventLoopGroup通常叫bossGroup和workerGroup一个负责接收连接一个负责处理读写几行代码就配置好了业界最优秀的线程模型。协议支持直接用 JDK NIO如果要实现 HTTP、WebSocket、SSL/TLS 加密传输你需要自己写成千上万行的解析代码。而 Netty 已经内置了这些协议的支持只需要在 Pipeline 中添加相应的 Handler 即可如HttpServerCodec、SslHandler。社区与验证Netty 经历了十多年的全球高并发场景洗礼。苹果、微博、阿里、腾讯、美团等大厂都在大规模使用。它的健壮性、稳定性和文档完善度是任何个人或单一团队自己封装 NIO 无法比拟的。如果直接用 JDK NIO这些团队需要组建专门的专家小组花费几个月甚至更久去解决 Epoll Bug、内存泄漏、粘包拆包、多线程调度等底层网络细节这严重分散了开发业务功能如 RPC 路由、消息存储、消费队列的精力。选择 Netty相当于直接站在了巨人的肩膀上获得了一个经过全球顶级流量验证、零 Bug已被规避、极高性能、极其稳定的网络通信底座。