Reactor模式学习总结

前言

      之前在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模式

常见的Reactor线程模式有三种,分别如下:

1、Reactor单线程模式

2、Reactor多线程模式

3、主从Reactor多线程模式

单线程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模式

在多线程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多线程模型。

主从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从单线程到多线程再到主从多线程也是如此,操作系统做了它应该做的事,用户程序也应该如此。

你可能感兴趣的:(netty,设计模式)