【Netty】——Reactor模式详解

我想大家都经历过学习优秀开源框架的痛苦,特别是第一次看源码,直接从一个类的方法一步步跳转下去的漩涡。极客时间里软件设计之美专栏提到:了解一个软件、框架的设计应该从三部分着手 ——模型、接口、实现。Netty的整体架构,基于了一个著名的模式——Reactor模式。Reactor模式,是高性能网络编程的必知必会模式,下面我们就从Reactor模式入手,来打开Netty学习的大门。


为何要用Reactor

最开始Java BIO网络编程思想是经典的connection per thread,即SocketServer while()循环接收客户端请求,每一个连接用一个线程处理,类似:

// 主线程维护连接
public void run() {

      try {
          while (true) {
              //阻塞等待连接
              Socket socket = serverSocket.accept();

              //方式一:开辟线程处理请求
              new Thread(new Handler(socket)).start()

              //方式二:优化,提交线程池处理
              //executorService.submit(new Handler(socket));
          }
      } catch (Exception e) {
          e.printStackTrace();
      }
}

​

// 处理读写服务
class Handler implements Runnable {

      public void run() {

          try {
              //获取Socket的输入流,接收数据
              BufferedReader buf = new BufferedReader(new InputStreamReader(socket.getInputStream()));
              //阻塞读取数据
              String readData = buf.readLine();
              while (readData != null) {
                  readData = buf.readLine();
                  System.out.println(readData);
              }
          } catch (Exception e) {
              e.printStackTrace();
          }      
      }
}

tomcat服务器的早期版本确实是这样实现的,一个socket客户端发起连接,server服务端在read阻塞以后,不会影响到后续的请求,因为他们在不同的线程中,这也是为什么通常会讲“一个线程只能对应一个socket”的原因,这种线程模型虽然一定程度上极大地提高了服务器的吞吐量,但很明显却存在以下弊端:

  1. 同步阻塞IO,读写阻塞,如果没有数据需要处理,线程将一致处于等待状态浪费系统资源。
  2. 一个连接开辟一个线程,诸如线程创建与销毁、多线程之间的上下文切换等问题,对服务器资源来说压力巨大,并且不易动态伸缩扩展
  3. 状态数据以及其他需要保持一致的数据,需要采用并发同步控制

如何解决呢?

NIO问世后,支持的基本机制:

  1. 非阻塞的IO读写,或多个IO连接公用一个阻塞对象
  2. 基于IO多路复用,对事件进行分发任务,一个线程同时支持对多个IO事件或通道的监听

什么是Reactor模式

其中Wikipedia上说:“The reactor design pattern is an event handling pattern for handling service requests delivered concurrently by one or more inputs. The service handler then demultiplexes the incoming requests and dispatches them synchronously to associated request handlers.”。从这个描述中,我们知道Reactor模式首先是

事件驱动的,

有一个或多个并发输入源,

有一个Service Handler,有多个Request Handlers

这个Service Handler会同步的将输入的请求(Event)多路复用的分发给相应的Request Handler。如果用图来表达:

反应器设计模式同步等待多个事件源到达(采用select()实现),然后将事件多路分解,并将它们分配到相关的请求处理程序,这个分派采用server集中处理(dispatch),分解的事件以及对应的事件服务应用从分派服务中分离出去(handler)。从结构上,这有点类似生产者消费者模式,即有一个或多个生产者将事件放入一个Queue中,而一个或多个消费者主动的从这个Queue中Poll事件来处理;而Reactor模式则并没有Queue来做缓冲,每当一个Event输入到Service Handler之后,该Service Handler会主动的根据不同的Event类型将其分发给对应的Request Handler来处理,也就是同步处理。

更学术的,这篇文章(Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events)上说:

“The Reactor design pattern handles service requests that are delivered concurrently to an application by one or more clients. Each service in an application may consistent of several methods and is represented by a separate event handler that is responsible for dispatching service-specific requests. Dispatching of event handlers is performed by an initiation dispatcher, which manages the registered event handlers. Demultiplexing of service requests is performed by a synchronous event demultiplexer. Also known as Dispatcher, Notifier”。

这段描述和Wikipedia上的描述类似,有多个输入源,有多个不同的EventHandler(RequestHandler)来处理不同的请求,Initiation Dispatcher用于管理EventHander,EventHandler首先要注册到Initiation Dispatcher中,然后Initiation Dispatcher根据输入的Event分发给注册的EventHandler;然而Initiation Dispatcher并不监听Event的到来,这个工作交给Synchronous Event Demultiplexer来处理。

OMT 类图设计

结合Java NIO 说明:

Handle:即操作系统中的句柄,是对资源在操作系统层面上的一种抽象,它可以是打开的文件、一个连接(Socket)、Timer等。由于Reactor模式一般使用在网络编程中,因而这里一般指Socket Handle,即一个网络连接(Connection,在Java NIO中的Channel)。这个Channel注册到Synchronous Event Demultiplexer中,以监听Handle中发生的事件,对ServerSocketChannnel可以是CONNECT事件,对SocketChannel可以是READ、WRITE、CLOSE事件等。
Synchronous Event Demultiplexer:阻塞等待一系列的Handle中的事件到来,如果阻塞等待返回,即表示在返回的Handle中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的select来实现。在Java NIO中用Selector来封装,当Selector.select()返回时,可以调用Selector的selectedKeys()方法获取Set<SelectionKey>,一个SelectionKey表达一个有事件发生的Channel以及该Channel上的事件类型。
Initiation Dispatcher:用于管理Event Handler,即EventHandler的容器,用以注册、移除EventHandler等;另外,它还作为Reactor模式的入口调用Synchronous Event Demultiplexer的select方法以阻塞等待事件返回,当阻塞等待返回时,根据事件发生的Handle将其分发给对应的Event Handler处理,即回调EventHandler中的handle_event()方法。Java NIO 中这部分一般由开发者编写,因为与业务逻辑紧密。
Event Handler:定义事件处理方法:handle_event(),以供InitiationDispatcher回调使用。
Concrete Event Handler:事件EventHandler接口,实现特定事件处理逻辑。

Reactor模式模块之间的交互

1. 初始化InitiationDispatcher,并初始化一个Handle到EventHandler的Map。
2. 注册EventHandler到InitiationDispatcher中,每个EventHandler包含对相应Handle的引用,从而建立Handle到EventHandler的映射(Map)。
3. 调用InitiationDispatcher的handle_events()方法以启动Event Loop。在Event Loop中,调用select()方法(Synchronous Event Demultiplexer)阻塞等待Event发生。
4. 当某个或某些Handle的Event发生后,select()方法返回,InitiationDispatcher根据返回的Handle找到注册的EventHandler,并回调该EventHandler的handle_events()方法。
5. 在EventHandler的handle_events()方法中还可以向InitiationDispatcher中注册新的Eventhandler,比如对AcceptorEventHandler来,当有新的client连接时,它会产生新的EventHandler以处理新的连接,并注册到InitiationDispatcher中。

Reactor模式实现

Reactor An Object Behavioral Pattern for Demultiplexing and Dispatching Handles for Synchronous Events中,一直以Logging Server来分析Reactor模式,这个Logging Server的实现完全遵循这里对Reactor描述,因而放在这里以做参考。Logging Server中的Reactor模式实现分两个部分:Client连接到Logging Server和Client向Logging Server写Log。因而对它的描述分成这两个步骤。

Client连接到Logging Server

1. Logging Server注册LoggingAcceptor到InitiationDispatcher。
2. Logging Server调用InitiationDispatcher的handle_events()方法启动。
3. InitiationDispatcher内部调用select()方法(Synchronous Event Demultiplexer),阻塞等待Client连接。
4. Client连接到Logging Server。
5. InitiationDisptcher中的select()方法返回,并通知LoggingAcceptor有新的连接到来。
6. LoggingAcceptor调用accept方法accept这个新连接。
7. LoggingAcceptor创建新的LoggingHandler。
8. 新的LoggingHandler注册到InitiationDispatcher中(同时也注册到Synchonous Event Demultiplexer中),等待Client发起写log请求。

Client向Logging Server写Log

1. Client发送log到Logging server。
2. InitiationDispatcher监测到相应的Handle中有事件发生,返回阻塞等待,根据返回的Handle找到LoggingHandler,并回调LoggingHandler中的handle_event()方法。
3. LoggingHandler中的handle_event()方法中读取Handle中的log信息。
4. 将接收到的log写入到日志文件、数据库等设备中。
3.4步骤循环直到当前日志处理完成。
5. 返回到InitiationDispatcher等待下一次日志写请求。

Java NIO对Reactor的实现

在Java的NIO中,对Reactor模式有无缝的支持,可以说是Reactor模型的朴素原型。Nio使用Selector类封装了操作系统提供的Synchronous Event Demultiplexer功能。这个Doug Lea已经在Scalable IO In Java中有非常深入的解释了,因而不再赘述,另外这篇文章对Doug Lea的Scalable IO In Java有一些简单解释。Java NIO 使用样例:

public class Server{

        public static void testServer() throws IOException{

            // 1、获取Selector选择器
            Selector selector = Selector.open();

            // 2、获取通道
            ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
            // 3.设置为非阻塞
            serverSocketChannel.configureBlocking(false);
            // 4、绑定连接
            serverSocketChannel.bind(new InetSocketAddress(SystemConfig.SOCKET_SERVER_PORT));

            // 5、将通道注册到选择器上,并注册的操作为:“接收”操作
            serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

            // 6、采用轮询的方式,查询获取“准备就绪”的注册过的操作
            while (selector.select() > 0){

                // 7、获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
                Iterator<SelectionKey> selectedKeys = selector.selectedKeys().iterator();
                while (selectedKeys.hasNext() {

                    // 8、获取“准备就绪”的事件
                    SelectionKey selectedKey = selectedKeys.next();

                    // 9、判断key是具体的什么事件
                    if (selectedKey.isAcceptable()){

                        // 10、若接受的事件是“接收就绪” 操作,就获取客户端连接
                        SocketChannel socketChannel = serverSocketChannel.accept();
                        // 11、切换为非阻塞模式
                        socketChannel.configureBlocking(false);
                        // 12、将该通道注册到selector选择器上
                        socketChannel.register(selector, SelectionKey.OP_READ);
                    
                    }else if (selectedKey.isReadable()){

                        // 13、获取该选择器上的“读就绪”状态的通道
                        SocketChannel socketChannel = (SocketChannel) selectedKey.channel();

                        // 14、读取数据
                        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                        int length = 0;
                        while ((length = socketChannel.read(byteBuffer)) != -1)
                        {
                            byteBuffer.flip();
                            System.out.println(new String(byteBuffer.array(), 0, length));
                            byteBuffer.clear();
                        }
                        socketChannel.close();
                    }

                    // 15、移除选择键
                    selectedKeys.remove();
                }
            }

            // 7、关闭连接
            serverSocketChannel.close();
        }

        public static void main(String[] args) throws IOException{
            testServer();
        }
    }

Reactor线程模型

在应用Java NIO构建Reactor Pattern中,大神 Doug Lea(在“Scalable IO in Java”中给了很好的阐述。

首先我们基于Reactor Pattern 处理模式中,定义以下三种角色:

Reactor 负责响应IO事件,当检测到一个新的事件,将其分派给相应的Handler去处理;新的事件包含连接建立就绪、读就绪、写就绪等。

Acceptor 处理客户端新连接,并分派请求到处理器链中

Handlers 将自身与事件绑定,负责事件的处理,执行非阻塞读/写 任务

单Reactor单线程模型

这是最简单的单Reactor单线程模型。Reactor线程是个多面手,负责多路分离套接字,Accept新连接,并分派请求到Handler处理器中。

代码样例:

class Reactor implements Runnable {

    final Selector selector;
    final ServerSocketChannel serverSocket;

    Reactor(int port) throws IOException { 
        
        //Reactor初始化
        selector = Selector.open();
        serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(port));
        //非阻塞
        serverSocket.configureBlocking(false);

        //分步处理,第一步,接收accept事件
        SelectionKey sk =
                serverSocket.register(selector, SelectionKey.OP_ACCEPT);
        //attach callback object, Acceptor
        sk.attach(new Acceptor());
    }

    public void run(){

        try{
            while (!Thread.interrupted()){
                selector.select();
                Set selected = selector.selectedKeys();
                Iterator it = selected.iterator();

                while (it.hasNext()){
                    //Reactor负责dispatch收到的事件
                    dispatch((SelectionKey) (it.next()));
                }
                selected.clear();
            }
        } catch (IOException ex){ 
           /* ... */ 
        }
    }

    void dispatch(SelectionKey k){
        Runnable r = (Runnable) (k.attachment());
        //调用之前注册的callback对象
        if (r != null){
            r.run();
        }
    }

    // inner class
    class Acceptor implements Runnable {

        public void run(){
            try{
                SocketChannel channel = serverSocket.accept();
                if (channel != null)
                    new Handler(selector, channel);
            } catch (IOException ex){ 
                /* ... */ 
            }
        }
    }
}

Handler的代码如下:

class Handler implements Runnable{

    final SocketChannel channel;
    final SelectionKey sk;
    ByteBuffer input = ByteBuffer.allocate(SystemConfig.INPUT_SIZE);
    ByteBuffer output = ByteBuffer.allocate(SystemConfig.SEND_SIZE);
    static final int READING = 0, SENDING = 1;
    int state = READING;

    Handler(Selector selector, SocketChannel c) throws IOException{
        channel = c;
        c.configureBlocking(false);
        // Optionally try first read now
        sk = channel.register(selector, 0);

        //将Handler作为callback对象
        sk.attach(this);

        //第二步,注册Read就绪事件
        sk.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    boolean inputIsComplete(){
        /* ... */
        return false;
    }

    boolean outputIsComplete(){

        /* ... */
        return false;
    }

    void process(){
        /* ... */
        return;
    }

    public void run(){
        try{
            if (state == READING){
                read();
            }
            else if (state == SENDING){
                send();
            }
        } catch (IOException ex){
          /* ... */ 
        }
    }

    void read() throws IOException{

        channel.read(input);
        if (inputIsComplete()){

            process();

            state = SENDING;
            // Normally also do first write now

            //第三步,接收write就绪事件
            sk.interestOps(SelectionKey.OP_WRITE);
        }
    }

    void send() throws IOException {
        channel.write(output);

        //write完就结束了, 关闭select key
        if (outputIsComplete()){
            sk.cancel();
        }
    }
}

这是最基本的单Reactor单线程模型。其中Reactor线程,负责多路分离套接字,有新连接到来触发connect 事件之后,交由Acceptor进行处理,有IO读写事件之后交给hanlder 处理。

Acceptor主要任务就是构建handler ,在获取到和client相关的SocketChannel之后 ,绑定到相应的hanlder上,对应的SocketChannel有读写事件之后,基于reacotor 分发,hanlder就可以处理了(所有的IO事件都绑定到selector上,有Reactor分发)。

单线程模型的缺点:

1、 当其中某个 handler 阻塞时, 会导致其他所有的 client 的 handler 都得不到执行, 并且更严重的是, handler 的阻塞也会导致整个服务不能接收新的 client 请求(因为 acceptor 也被阻塞了)。 因为有这么多的缺陷, 因此单线程Reactor 模型用的比较少。这种单线程模型不能充分利用多核资源,所以实际使用的不多。

2、因此,单线程模型仅仅适用于handler 中业务处理组件能快速完成的场景。

单Reactor多线程模型

在单线程Reactor模式基础上,做如下改进:

(1)将Handler处理器的执行放入线程池,多线程进行业务处理。

(2)而对于Reactor而言,可以仍为单个线程。如果服务器为多核的CPU,为充分利用系统资源,可以将Reactor拆分为两个线程。

改进的Handler的代码如下:

class MthreadHandler implements Runnable{
    final SocketChannel channel;
    final SelectionKey selectionKey;
    ByteBuffer input = ByteBuffer.allocate(SystemConfig.INPUT_SIZE);
    ByteBuffer output = ByteBuffer.allocate(SystemConfig.SEND_SIZE);
    static final int READING = 0, SENDING = 1;
    int state = READING;


    ExecutorService pool = Executors.newFixedThreadPool(4);
    static final int PROCESSING = 3;

    MthreadHandler(Selector selector, SocketChannel c) throws IOException{
        channel = c;
        c.configureBlocking(false);
        // Optionally try first read now
        selectionKey = channel.register(selector, 0);

        //将Handler作为callback对象
        selectionKey.attach(this);

        //第二步,注册Read就绪事件
        selectionKey.interestOps(SelectionKey.OP_READ);
        selector.wakeup();
    }

    boolean inputIsComplete(){
       /* ... */
        return false;
    }

    boolean outputIsComplete(){
       /* ... */
        return false;
    }

    void process(){
       /* ... */
        return;
    }

    public void run(){
        try
        {
            if (state == READING)
            {
                read();
            }
            else if (state == SENDING)
            {
                send();
            }
        } catch (IOException ex)
        { /* ... */ }
    }


    synchronized void read() throws IOException{
        // ...
        channel.read(input);
        if (inputIsComplete())
        {
            state = PROCESSING;
            //使用线程pool异步执行
            pool.execute(new Processer());
        }
    }

    void send() throws IOException{
        channel.write(output);

        //write完就结束了, 关闭select key
        if (outputIsComplete()){
            selectionKey.cancel();
        }
    }

    synchronized void processAndHandOff(){
        process();
        state = SENDING;
        // or rebind attachment
        //process完,开始等待write就绪
        selectionKey.interestOps(SelectionKey.OP_WRITE);
    }

    class Processer implements Runnable{
        public void run(){
            processAndHandOff();
        }
    }

}

Reactor 类没有大的变化,参考前面的代码。

此模型可以充分利用多核CPU的处理能力,但是多线程数据共享和访问会比较复杂,Reactor在单线程里处理所有连接与事件的监听,以及事件分发,在高并发场景容易出现性能瓶颈,多线程仅仅解决业务处理的压力。

多Reactor多线程模型

对于多个CPU的机器,为充分利用系统资源,将Reactor拆分为两部分。

1.mainReactor负责监听server socket,用来处理新连接的建立,将建立的socketChannel指定注册给subReactor。

subReactor维护自己的selector, 基于mainReactor 注册的socketChannel多路分离IO读写事件,读写网 络数据,对业务处理的功能,另其扔给worker线程池来完成。

class MthreadReactor implements Runnable{

    //subReactors集合, 一个selector代表一个subReactor
    Selector[] selectors=new Selector[2];
    int next = 0;
    final ServerSocketChannel serverSocket;

    MthreadReactor(int port) throws IOException{ 
        //Reactor初始化
        selectors[0]= Selector.open();
        selectors[1]= Selector.open();
        serverSocket = ServerSocketChannel.open();
        serverSocket.socket().bind(new InetSocketAddress(port));
        //非阻塞
        serverSocket.configureBlocking(false);

        //分步处理,第一步,mainReacotr侦听accept事件
        SelectionKey sk =
                serverSocket.register( selectors[0], SelectionKey.OP_ACCEPT);
        //attach callback object, Acceptor
        sk.attach(new Acceptor());
    }

    public void run(){
        try{
            while (!Thread.interrupted()){
                for (int i = 0; i <2 ; i++){
                    selectors[i].select();
                    Set selected =  selectors[i].selectedKeys();
                    Iterator it = selected.iterator();
                    while (it.hasNext()){
                        //Reactor负责dispatch收到的事件
                        dispatch((SelectionKey) (it.next()));
                    }
                    selected.clear();
                }
            }
        } catch (IOException ex){
           /* ... */ 
        }
    }

    void dispatch(SelectionKey k){
        Runnable r = (Runnable) (k.attachment());
        //调用之前注册的callback对象
        if (r != null){
            r.run();
        }
    }


    class Acceptor { // ...
        public synchronized void run() throws IOException{
            //主selector负责accept
            SocketChannel connection =serverSocket.accept(); 
            if (connection != null)
            {
                //选个subReactor去负责接收到的connection
                new MthreadHandler(selectors[next], connection); 
            }
            if (++next == selectors.length) next = 0;
        }
    }
}

Reactor编程的优点和缺点

优点

1)响应快,不必为单个同步时间所阻塞,虽然Reactor本身依然是同步的;

2)编程相对简单,可以最大程度的避免复杂的多线程及同步问题,并且避免了多线程/进程的切换开销;

3)可扩展性,可以方便的通过增加Reactor实例个数来充分利用CPU资源;

4)可复用性,reactor框架本身与具体事件处理逻辑无关,具有很高的复用性;

缺点

1)相比传统的简单模型,Reactor增加了一定的复杂性,因而有一定的门槛,并且不易于调试。

2)Reactor模式需要底层的Synchronous Event Demultiplexer支持,比如Java中的Selector支持,操作系统的select系统调用支持,如果要自己实现Synchronous Event Demultiplexer可能不会有那么高效。

3) Reactor模式在IO读写数据时还是在同一个线程中实现的,即使使用多个Reactor机制的情况下,那些共享一个Reactor的Channel如果出现一个长时间的数据读写,会影响这个Reactor中其他Channel的相应时间,比如在大文件传输时,IO操作就会影响其他Client的相应时间,因而对这种操作,使用传统的Thread-Per-Connection或许是一个更好的选择,或则此时使用改进版的Reactor模式如Proactor模式

参考:【Netty】——Reactor模式详解

posted @ 2022-04-15 15:55  aspirant  阅读(717)  评论(0编辑  收藏  举报