三种 IO 模式和对应的开发模式如下:
BIO | NIO | AIO |
---|---|---|
Thread-Per-Connection | Reactor | Proactor |
Reactor 是一种开发模式,核心流程为:
1、注册感兴趣的事件
2、扫描是否有感兴趣的事件发生
3、事件发生后做相应的处理
简言之,注册事件(register)、扫描事件(select)、分发事件(dispatch)、处理事件(handle)。
❀ 我们发现 Reactor 整个模式从始至终都围绕着事件,下面表格对应了 Netty 中不同的 Channel 监听的事件。
client/server | SocketChannel/ServerSocketChannel | OP_ACCEPT | OP_CONNECT | OP_READ | OP_WRITE |
---|---|---|---|---|---|
client | SocketChannel | Y | Y | Y | |
server | ServerSocketChannel | Y | |||
server | SocketChannel | Y | Y |
accept()
方法并创建对应的 SocketChannel 。上面这些事件注册完成后,会有一个多路复用器一直扫描,在监听到这些事件后,Netty 会作相应的处理。
在网络编程中,如果每个客户端都与服务器保持一个连接,对于服务器来说是一个很大的压力。比如 BIO ,也就是上面表格的单线程模式:
上面的三个客户端与服务端之间有三个链接,对于每个链接都有一个 Handler 处理相应的事件:读,解码,计算,编码,响应,也就是占用了三个线程。而其中的读和响应都是阻塞线程的,这时一个严重的问题就出现了,阻塞的连接越多,占用的线程越多。
这种方式,抽象成代码就是:
我们来看 Reactor 如何解决上述出现的问题。
单线程比较简单,所有的工作都由一个线程来做,包括接收连接、注册事件、扫描、分发、处理等。虽然解决了阻塞问题,但是单线程效率不高,一旦这个线程挂掉,整个系统就 game over 了,这是不能忍受的。
为了解决单线程的问题,我们引入了线程池。在这个模式中,把比较耗时的 decode、compute、encode 操作交给线程池来做。与单线程模式对比,效率明显提升。
在上一版本的基础上,使用主从模式,新增主 Reactor 单独处理接收连接,然后将建立的 SocketChannel 注册给指定的从 Reactor。
在不指定线程数时,会根据 CPU 的核数来计算最优线程数。第三种主从模式,bossGroup 负责接收连接并将 SocketChannel 注册到 workGroup 上,woekGroup 负责处理其他事件。