NIO
- 三大核心:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
原生NIO存在的问题
- NIO的类库和API繁琐,使用麻烦:需要熟练掌握Selector、ServerSocketChannel、ServerSocket、ByteBuffer等。
- 需要具备其他的额外技能:要熟悉 Java 多线程编程,因为 NIO 编程涉及到 Reactor 模式,你必须对多线程和网络编程非常熟悉,才能编写出高质量的 NIO 程序。
- 开发工作量和难度都非常大:例如客户端面临断线重连、网络闪断、半包读写、失败缓存、网络堵塞和异常流的处理等等。
- JDK NIO 的 Bug:例如臭名昭著的 Epoll Bug,它会导致 Selector 空轮询,最终导致 CPU 100%。直到 JDK 1.7 版本该问题仍旧存在,没有被根本解决。
线程模型
单 Reactor 模式

单 Reactor 单线程

单 Reactor 多线程

优点:可以充分的利用多核cpu的处理能力。
缺点:多线程数据共享和访问比较复杂,reactor 处理所有的事件的监听和响应,在单线程运行,在高并发场景容易出现性能瓶颈。
主从 Reactor 多线程

- Reactor 主线程 MainReactor 对象通过 select 监听链接事件,收到时间后,通过 Acceptor 处理链接事件。
- 当 Acceptor 处理链接事件后,MainReactor 将连接分配给 SubReactor.
- subreactor 将连接加入到连接队列进行监听,并创建 handler 进行各种事件处理。
- 当有新事件发生时,subreactor 就会调用对应的 handler 处理。
- handler 通过 read 读取数据,分发给后面的 worker 线程处理。
- worker 线程池分配独立的 worker 线程进行业务处理,并返回结果。
- handler 收到响应的结果后,再通过 send 将结果返回给client。
- Reactor 主线程可以对应多个 Reactor 子线程,即 MainReactor 可以关联多个 subreactor
方案优缺点
优点:
- 父线程与子线程的数据交互简单职责明确,父线程只需要接收新连接,子线程完成后续的业务处理。
- 父线程与子线程的数据交互简单,Reactor 主线程只需要把新连接传给子线程,子线程无需返回数据。
缺点:
- 编程复杂度较高
结合实例:这种模型在许多项目中广泛使用,包括 Nginx 主从 Reactor 多进程模型,Memcached 主从多线程,Netty 主从多线程模型的支持。
Scalable IO in Java 书中作者 Doug Lea 对 Reactor 模型的理解
Netty 模型

工作原理示意图-解析
- Netty抽象出两组线程池BossGroup专门负责接收客户端的连接,WorkerGroup专门负责网络的读写。
- BossGroup和WorkerGroup类型都是NioEventLoopGroup
- NioEventLoopGroup相当于一个事件循环组,这个组中含有多个事件循环,每个事件循环是NioEventLoop
- NioEventLoop 表示一个不断循环的执行处理任务的线程,每个NioEventLoop都有一个selector,用于监听绑定在其上的socket的网络通讯
- NioEventLoopGroup 可以有多个线程,即可以含有多个NioEventLoop
- 每个Boss NioEventLoop 执行的步骤有3步:
- 轮询accept事件
- 处理accept事件,与client建立连接,生成 NioSocketChannel,并将其注册到某个worker NIOEventLoop 上的selector
- 处理任务队列的任务,即 runAllTasks
- 每个Worker NIOEventLoop 循环执行的步骤
- 轮询read,write事件
- 处理i/o事件,即read,write事件,在对应 NioSocketChannel 处理
- 处理任务队列的任务,即 runAllTasks
- 每个Worker NIOEventLoop 处理业务时,会使用pipeline(管道),pipeline 中包含了 channel,即通过pipeline 可以获取到对应通道,管道中维护了很多的处理器。
Protobuf
http + json -> tcp + protobuf
Protobuf优点:
- 序列化后体积相比Json和XML很小,适合网络传输
- 支持跨平台多语言
- 消息格式升级和兼容性还不错
- 序列化反序列化速度很快,快于Json的处理速速
Protobuf缺点:
- 二进制格式导致可读性差
- 缺乏自描述
- 通用性差
Handler
- 不论解码器handler还是编码器handler,接收的消息类型必须与待处理的消息类型一致,否则该handler不会被执行
- 在解码器进行数据解码时,需要判断缓存区(ByteBuf)的数据是否足够,否则接收到的结果与期望结果可能不一致