开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第21天,点击查看活动详情
3.Netty
- Netty 是一个基于 NIO 基础开发的网络通讯框架,支持快速、轻松地开发网络应用程序,如协议服务器和客户机。大大简化了网络编程,如 TCP 和 UDP 套接字服务器。
3.1.Netty设计思想
3.1.1.事件驱动模型
-
通常,我们设计一个事件处理模型的程序有两种思路:
- 轮询方式,线程不断轮询访问相关事件发生源有没有发生事件,有发生事件就调用事件处理逻辑。
- 事件驱动方式,发生事件,主线程把事件放入事件队列,在另外线程不断循环消费事件列表中的事件,调用事件对应的处理逻辑处理事件。事件驱动方式也被称为消息通知方式,其实是设计模式中观察者模式的思路。
-
以 GUI 的逻辑处理为例,说明两种逻辑的不同:
- 轮询方式,线程不断轮询是否发生按钮点击事件,如果发生,调用处理逻辑。
- 事件驱动方式,发生点击事件把事件放入事件队列,在另外线程消费的事件列表中的事件,根据事件类型调用相关事件处理逻辑。
3.1.2.Reactor模型
-
网络框架大多数都是基于 Reactor 模式进行设计和开发,Reactor 模式又基于事件驱动,特别适合处理海量的 I/O 事件。
-
单线程模型:Reactor单线程模型,指的是所有的IO操作都在同一个NIO线程上面完成,NIO线程的职责如下:
- 作为NIO服务端,接收客户端的TCP连接;
- 作为NIO客户端,向服务端发起TCP连接;
- 读取通信对端的请求或者应答消息;
- 向通信对端发送消息请求或者应答消息。
由于 Reactor 模式使用的是同步非阻塞 IO,所有的 IO 操作都不会导致阻塞,理论上一个线程可以独立处理所有 IO 相关的操作。从架构层面看,一个 NIO 线程确实可以完成其承担的职责。例如,通过 Acceptor 类接收客户端的 TCP 连接请求消息,链路建立成功之后,通过 Dispatch 将对应的 ByteBuffer 派发到指定的 Handler 上进行消息解码。用户线程可以通过消息编码通过 NIO 线程将消息发送给客户端。
对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用场景却不合适,主要原因如下:
- 一个 NIO 线程同时处理成百上千的链路,性能上无法支撑,即便 NIO 线程的 CPU 负荷达到 100% ,也无法满足海量消息的编码、解码、读取和发送;
- 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,成为系统的性能瓶颈;
- 可靠性问题:一旦 NIO 线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。
为了解决这些问题,演化出了 Reactor 多线程模型。
-
多线程模型:Rector 多线程模型与单线程模型最大的区别就是有一组 NIO 线程处理 IO 操作,Reactor 多线程模型的特点:
- 有专门一个 NIO Acceptor 线程用于监听服务端,接收客户端的 TCP 连接请求;
- 网络 IO 操作读、写等由一个 NIO 线程池负责,线程池可以采用标准的 JDK 线程池实现,它包含一个任务队列和 N 个可用的线程,由这些 NIO 线程负责消息的读取、解码、编码和发送;
- 一个 NIO 线程可以同时处理 N 条链路,但是一个链路只对应一个 NIO 线程,防止发生并发操作问题。
在绝大多数场景下,Reactor 多线程模型都可以满足性能需求;但是,在极个别特殊场景中,一个 NIO 线程负责监听和处理所有的客户端连接可能会存在性能问题。例如并发百万客户端连接,或者服务端需要对客户端握手进行安全认证,但是认证本身非常损耗性能。在这类场景下,单独一个 Acceptor 线程可能会存在性能不足问题,为了解决性能问题,产生了第三种 Reactor 线程模型:主从Reactor多线程模型。
-
主从多线程模型:服务端用于接收客户端连接的不再是一个单独的 NIO 线程,而是一个独立的 NIO 线程池
Acceptor 接收到客户端 TCP 连接请求处理完成后 (可能包含接入认证等) ,将新创建的 SocketChannel 注册到 IO 线程池 (sub reactor线程池) 的某个 IO 线程上,由它负责 SocketChannel 的读写和编解码工作。Acceptor 线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor 线程池的 IO 线程上,由 IO 线程负责后续的 IO 操作。