之前在JAVA IO中已经提到过reactor模式,reactor可以说是nio的核心,也是netty之所以高效的原因之一,今天我们来总结下reactor模式相关的知识点。在开始学习之前,我们需要了解常用IO模型的两种体系结构:
1、thread-based architecture(基于线程):如BIO,一个客户端请求(连接)对应一个独立线程
2、event-driven architecture(事件驱动):如NIO,定义一系列的事件处理器来响应事件的发生,并且将服务端接受连接与对事件的处理分离,其本质就是细化请求粒度,比如将一次连接中的解码、读、写分离开来。
操作系统能为我们做的事都是一样的,当一次发起一次IO请求后,如何处理或者说如何将资源分配的更为合理,往往是决定系统性能的关键。比如当B和N两个人同时发起了IO请求,B就在那儿啥啥的等着操作系统把数据给他送到内核空间,而N的原则事,发起请求后则去做手上其他的事,时不时看看数据来了没,提高CPU的利用率。这就是典型的事件驱动模型,Reactor模式就是事件驱动模型的一种。
常见的Reactor线程模式有三种,分别如下:
1、Reactor单线程模式
2、Reactor多线程模式
3、主从Reactor多线程模式
在上一篇中NIO中已经介绍过java传统的NIO模式:
public void run() {
//循环遍历selector
while(started){
try{
//无论是否有读写事件发生,selector每隔1s被唤醒一次
selector.select(1000);
Set keys = selector.selectedKeys();
Iterator it = keys.iterator();
SelectionKey key = null;
while(it.hasNext()){
key = it.next();
it.remove();
try{
handleInput(key);
}catch(Exception e){
if(key != null){
key.cancel();
if(key.channel() != null){
key.channel().close();
}
}
}
}
}catch(Throwable t){
t.printStackTrace();
}
}
//selector关闭后会自动释放里面管理的资源
if(selector != null)
try{
selector.close();
}catch (Exception e) {
e.printStackTrace();
}
}
即启动一个Reactor线程(如上代码段),负责监听指定端口的事件(selectionKey绑定的事件),如果有事件,则交给事件处理器(handleInput)去处理,所有的IO操作都交给这个单独的NIO线程去处理(这里的NIO线程指的是使用NIO Channel的线程,支持异步的操作,在单线程Reactor模式中,即Reactor线程),原则就是做完自己该做的事就不管了,如果有下一个事件要去做,就马上去做下一件事。服务端启动代码段如下:
public static synchronized void start(int port){
if(serverHandler!=null)
serverHandler.stop();
serverHandler = new ServerHandler(port);
new Thread(serverHandler,"Server").start();
}
public static void main(String[] args){
System.out.println("NIO服务器启动---------");
start();
}
通过启动一个Reactor线程去监听,并采用异步的方式处理IO操作,这在一定程度上解决了盲目等待的问题。然而这种方式依然是单核模型,整个流程的处理(连接、解码、读写)都交给一个线程去处理,对于一些小容量应用场景,可以使用单线程模型,但是对于高负载、大并发的应用却不合适,也没有充分利用多核的资源。
在多线程Reactor模式中多了一个acceptor的角色,用于accpet连接,而具体的IO操作交给一个NIO线程池去处理。Accpetor示例如下:
public class Acceptor implements Runnable{
private Reactor reactor;
public Acceptor(Reactor reactor){
this.reactor=reactor;
}
@Override
public void run() {
try {
SocketChannel socketChannel=reactor.serverSocketChannel.accept();
if(socketChannel!=null)//调用Handler来处理channel
new SocketReadHandler(reactor.selector, socketChannel);
} catch (IOException e) {
e.printStackTrace();
}
}
}
Acceptor负责执行acceppt(),当监听的端口建立的NIO Chanel不为0;则新建一个NIO线程加入到NIO线程池中。在绝大多数场景下,Reactor多线程模型都可以满足性能需求;但是,在极特殊应用场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如百万客户端并发连接,或者服务端需要对客户端的握手信息进行安全认证,认证本身非常损耗性能。这类场景下,单独一个Acceptor线程可能会存在性能不足问题,为了解决性能问题,产生了第三种Reactor线程模型--主从Reactor多线程模型。
服务端用于接收客户端连接的不再是1个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到I/O线程池(sub reactor线程池)的某个I/O线程上,由它负责SocketChannel的读写和编解码工作。依然以上一篇中NIO Netty实现代码为例:
public NettyServer(int port) {
this.port = port;
}
public void start() throws Exception {
EventLoopGroup bossGroup = new NioEventLoopGroup();
EventLoopGroup group = new NioEventLoopGroup();
try {
ServerBootstrap sb = new ServerBootstrap();
sb.option(ChannelOption.SO_BACKLOG, 1024);
sb.group(group, bossGroup) // 绑定线程池
.channel(NioServerSocketChannel.class) // 指定使用的channel
.localAddress(this.port)// 绑定监听端口
.childHandler(new ChannelInitializer() { // 绑定客户端连接时候触发操作
@Override
protected void initChannel(SocketChannel ch) throws Exception {
System.out.println("收到新连接");
ch.pipeline().addLast(new StringEncoder(Charset.forName("GBK")));
ch.pipeline().addLast(new NettyServerHandler()); // 客户端触发操作
ch.pipeline().addLast(new ByteArrayEncoder());
}
});
ChannelFuture cf = sb.bind().sync(); // 服务器异步创建绑定
System.out.println(NettyServer.class + " 启动正在监听: " + cf.channel().localAddress());
cf.channel().closeFuture().sync(); // 关闭服务器通道
}
finally {
group.shutdownGracefully().sync(); // 释放线程池资源
bossGroup.shutdownGracefully().sync();
}
}
Netty中的Boss类充当mainReactor,NioWorker类充当subReactor(默认的个数即可用核数)。在处理新来的请求 时,NioWorker读完已收到的数据到ChannelBuffer中,之后触发ChannelPipeline中的ChannelHandler流。其实对比多线程Reactor模式,主从多线程的方式就是将accept的过程也交给线程池处理,将多核CPU利用到Accept阶段。
合理的设计IO模型可用有效的提高CPU利用率,NIO相对于BIO也是在做这样的事,而Reactor从单线程到多线程再到主从多线程也是如此,操作系统做了它应该做的事,用户程序也应该如此。