6.【nio】Reactor模式

72 阅读6分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 2 月更文挑战」的第 7 天,点击查看活动详情

前文如上:

4.【nio】IO多路复用-select、poll、epoll - 掘金 (juejin.cn)

5.【nio】四种主要的IO模型

文档参考: 《Java高并发核心编程 卷1:NIO、Netty、Redis、ZooKeeper》

为什么学习Reactor模式?

为什么在学习Netty之前首先要学习Reactor模式呢?

资深程序员都知道,Java程序不是按照顺序执行的逻辑来组织的。代码中所用到的设计模式在一定程度上已经演变成代码的组织方式。越是高水平的Java代码,抽象的层次越高,到处都是高度抽象和面向接口的调用,大量用到继承、多态、设计模式。

在阅读别人的源代码时,如果不了解代码所使用的设计模式,往往会晕头转向,不知身在何处,对代码跟踪和阅读都很成问题。反过来,如果先掌握到代码的设计模式,再去阅读代码,其过程就会变得很轻松,代码也就不会那么难懂了。当然,在编写代码时,如果不能熟练地掌握设计模式,也很难写出高水平的Java代码。

在学习和掌握高并发通信过程时,会学到很多高并发通信的框架,比如Netty, Netty本身很抽象,大量应用了设计模式。所以,学习像Netty这样的“精品中的精品”框架也是需要先从设计模式入手的,而Netty的整体架构是基于Reactor模式的

什么是Reactor模式?

简介

站在巨人的肩膀上,首先引用一下Doug Lea大师在文章“Scalable IO in Java”中对Reactor模式的定义

Reactor模式由Reactor线程、Handlers处理器两大角色组成,两大角色的职责分别如下:

(1)Reactor线程的职责:负责响应IO事件,并且分发到Handlers处理器。

(2)Handlers处理器的职责:非阻塞的执行业务处理逻辑。

从上面的Reactor模式定义中看不出这种模式有什么神奇的地方。当然,从简单到复杂,Reactor模式也有很多版本,前面的定义仅仅是最为简单的一个版本。如果需要彻底了解Reactor模式,还得从最原始的OIO编程开始讲起。

多线程OIO的致命缺陷

在Java的OIO编程中,原始的网络服务器程序一般使用一个while循环不断地监听端口是否有新的连接。如果有,就调用一个处理函数来完成传输处理。示例代码如下:

while(true){    

socket = accept();

//阻塞,接收连接    

handle(socket) ;  

//读取数据、业务处理、写入结果

}

这种方法的最大问题是:如果前一个网络连接的handle(socket)没有处理完,那么后面的新连接无法被服务端接收,于是后面的请求就会被阻塞,导致服务器的吞吐量太低。这对于服务器来说是一个严重的问题。

为了解决这个严重的连接阻塞问题,出现了一个极为经典的模式:Connection Per Thread(一个线程处理一个连接)模式。(参照前文提到的四种io模型:同步阻塞io)示例代码如下:

package com.crazymakercircle.iodemo.OIO;

//省略import导入的Java类

class ConnectionPerThread implements Runnable {

   public void run() {

       try {

           //服务器监听socket

           ServerSocketserverSocket =

                   new ServerSocket(NioDemoConfig.SOCKET_SERVER_PORT);

           while (!Thread.interrupted()) {

               Socket socket = serverSocket.accept();

               //接收一个连接后,为socket连接,新建一个专属的处理器对象

               Handler handler = new Handler(socket);

               //创建新线程,专门负责一个连接的处理

               new Thread(handler).start();

           }

       } catch (IOException ex) { /* 处理异常 */ }

   }

   //处理器,这里将内容回显到客户端

   static class Handler implements Runnable {

       final Socket socket;

       Handler(Socket s) {

           socket = s;

       }

       public void run() {

           while (true) {

               try {

                   byte[] input = new byte[1024];

                   /* 读取数据 */

                   socket.getInputStream().read(input);

                   /* 处理业务逻辑,获取处理结果*/

                   byte[] output =null;

                   /* 写入结果 */

                   socket.getOutputStream().write(output);

               } catch (IOException ex) { /处理异常/ }

           }

       }

   }

}

以上示例代码中,对于每一个新的网络连接都分配给一个线程。每个线程都独自处理自己负责的socket连接的输入和输出。当然,服务器的监听线程也是独立的,任何socket连接的输入和输出处理都不会阻塞到后面新socket连接的监听和建立,这样服务器的吞吐量就得到了提升。早期版本的Tomcat服务器就是这样实现的。

Connection Per Thread模式(一个线程处理一个连接)的优点是解决了前面的新连接被严重阻塞的问题,在一定程度上较大地提高了服务器的吞吐量。

Connection Per Thread模式的缺点是对应于大量的连接,需要耗费大量的线程资源,对线程资源要求太高。在系统中,线程是比较昂贵的系统资源。如果线程的数量太多,系统将无法承受。而且,线程的反复创建、销毁、切换也需要代价。因此,在高并发的应用场景下,多线程OIO的缺陷是致命的。

新的问题来了:如何减少线程数量?比如说让一个线程同时负责处理多个socket连接的输入和输出,行不行?看上去没有什么不可以,实际上作用不大。因为在传统OIO编程中每一次socket传输的IO读写处理都是阻塞的。在同一时刻,一个线程里只能处理一个socket的读写操作,前一个socket操作被阻塞了,其他连接的IO操作同样无法被并行处理。所以,在OIO中,即使是 一个线程同时负责处理多个socket连接的输入和输出,同一时刻该线程也只能处理一个连接的IO操作。 (所以就需要这个线程去不断轮询一个连接内的io读写状态,比如前文中提到的 同步非阻塞IO,每个用户 线程需要不断地进行IO系统调用,轮询数据是否已经准备好, 但是这么做还是有问题 ,就是 同一时刻该用户线程也只能处理这一个用户连接的IO操作,如何让某个线程去处理多个io的读写状态呢?

如何解决Connection Per Thread模式的巨大缺陷呢?一个有效途径是使用Reactor模式。用Reactor模式对线程的数量进行控制,做到一个线程处理大量的连接。那么它是如何做到的呢?下回分析——单线程的Reactor模式。