(五)Tomcat架构及运行原理之IO多路复用

目录

一、多路复用

1.概念引入

2.BIO处理并发情况

3.多路复用IO并发情况

4.两者的对比

二、多路复用模型

1.Selector和Poll模型

2.epoll模型(仅限Linux系统)

3.Reactor多线程模型

三、Tomcat之IO多路复用应用

1.模型流程

2.模型初始化源码分析

2.1 ProtocolHandler之Http11NioProtocol初始化

2.2 Endpoint和Accptor初始化

3.模型运行源码分析

3.1 Accptor接收器

3.2 Reactor容器之Endpoint

3.3 处理IO多路复用的Poller和PollerEvent

3.4 执行用户请求的线程对象SocketProcessorBase


一、多路复用

1.概念引入

如果想知道Tomcat对于IO多路复用的应用,就应该先了解IO多路复用是个什么概念。假设系统现在需要处理两个互相独立的IO事件:A事件和B事件,并且这两个事件的触发时间是随机的,那么系统应该如何处理A和B呢?对于传统的BIO而言,要处理这两个事件那么只能创建两个线程来分别监听处理,但是如果事件的数量是动态变化的,那么使用常规BIO将会损耗过多的系统资源。而如果我们的入口线程只有一个,优先处理哪个事件将成为难题。IO多路复用便是为了处理这种场景而出现的。

IO多路复用:IO就是指的我们网络IO,多路指多个TCP连接(或多个Channel),复用指复用一个或少量线程。串起来理解就是很多个网络IO复用一个或少量的线程来处理这些连接。

2.BIO处理并发情况

常规的BIO同时请求A和B事件的大致示意图:

(五)Tomcat架构及运行原理之IO多路复用_第1张图片

因为BIO是直接开始处理流的,当没有处理完时其它的线程请求时将会被阻塞,请求无法被程序接收,从而导致系统资源的浪费。使用BIO如果想要满足多个请求,只能创建更多的线程来处理,但如果对于服务器而言,任意时刻请求的数量大多是不可预估的,且线程之间的上下文切换开销也会是个问题,这就意味着要么浪费资源性能来空耗,要么就让服务器的并发量很低。

3.多路复用IO并发情况

使用多路复用IO同时请求A和B事件的大致示意图:

(五)Tomcat架构及运行原理之IO多路复用_第2张图片

和常规的BIO不同的是,多路复用使用了Selector复用器来对请求进来的事件进行统一的注册分发,随后再使用一个或者少量线程来对IO请求进行真正的处理,而不是让请求B一直阻塞在那里,直到A整个请求完成。

4.两者的对比

可能看到这个图只是知道BIO和多路复用IO的大致区别,对于多路复用IO和BIO都是需要阻塞的,我们就需要知道各自是在哪里阻塞的,阻塞的效果是什么。

  • BIO:阻塞的是操作系统资源,当占用时其它的线程无法使用,只能阻塞,因此多个线程进来时只能有一个请求被处理,直到这个请求被处理完才可以处理其它的请求;
  • 多路复用IO:阻塞的是用应用程序层面,不阻塞操作系统资源,因此多个请求进来时虽然被阻塞,但当请求就绪时,就会使用操作系统资源直接复制,复制后再后续处理其它就绪的请求。

引用网上一篇文章的话:从应用进程的角度去理解始终是阻塞的,等待数据和将数据复制到用户进程这两个阶段都是阻塞的。这一点我们从应用程序是可以清楚的得知,比如我们调用一个以I/O复用为基础的NIO应用服务。调用端是一直阻塞等待返回结果的。

从操作系统的角度等待Selector上面的网络事件就绪,是阻塞的,如果没有任何一个网络事件就绪则一直等待直到有一个或者多个网络事件就绪。但是从操作系统的角度考虑,有一点是不阻塞的,就是复制数据,因为操作系统不用等待,当有就绪条件满足的时候,它直接复制,其余时间在处理别的就绪的条件。这也是大家一直说的非阻塞I/O。实际上是就是指的这个地方的非阻塞。

二、多路复用模型

1.Selector和Poll模型

学过JDK的NIO应该熟悉这种模型,这种模型一般适用于以下场景:

  1. 当客户处理多个描述字时(一般是交互式输入和网络套接口),必须使用I/O复用;
  2. 当一个客户同时处理多个套接口时,而这种情况是可能的,但很少出现;
  3. 如果一个TCP服务器既要处理监听套接口,又要处理已连接套接口,一般也要用到I/O复用;
  4. 如果一个服务器即要处理TCP,又要处理UDP,一般要使用I/O复用;
  5. 如果一个服务器要处理多个服务或多个协议,一般要使用I/O复用。

与传统的BIO相比,这种模型的优势便在于系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。JDK对其实现的示意图如下:

(五)Tomcat架构及运行原理之IO多路复用_第3张图片

这种模式的缺陷:

  1. 单个进程能够监视的文件描述符的数量存在最大限制,通常是1024(Window和Linux默认的都是1024),当然可以更改数量,但由于select采用轮询的方式扫描文件描述符,文件描述符数量越多,性能越差;
  2. 内核 / 用户空间内存拷贝问题,select需要复制大量的句柄数据结构,产生巨大的开销;
  3. select返回的是含有整个句柄的数组,应用程序需要遍历整个数组才能发现哪些句柄发生了事件;
  4. select的触发方式是水平触发,应用程序如果没有完成对一个已经就绪的文件描述符进行IO操作,那么之后每次select调用还是会将这些文件描述符通知进程。

Poll使用的是链表保存文件描述符,因此不存在第一个缺陷,但其它三个缺点依然存在。

2.epoll模型(仅限Linux系统)

epoll模型便是Linux用来解决前面提到select和poll两种模型缺陷的增强版,这种模型一般被用于高并发服务型程序,特别是在大量并发连接中只有少部分连接处于活跃下的情况。epoll采用的是事件驱动,不用像select和poll去遍历监听描述符集合中的所有文件描述符,而是遍历那些被操作系统IO事件异步唤醒后加入到就绪队列,并返回到用户空间的描述符集合。

由于Springboot项目和Tomcat项目基本没用到epoll模型,因此只做简单的带过。

3.Reactor多线程模型

Reactor模型基于事件驱动,特别适合处理海量的I/O事件。Reactor模型中定义的三种角色:

  1. Acceptor:处理客户端新连接,并分派请求到处理器链中;
  2. Reactor:负责监听和分配事件,将I/O事件分派给对应的Handler。新的事件包含连接建立就绪、读就绪、写就绪等;
  3. Handler:将自身与事件绑定,执行非阻塞读/写任务,完成channel的读入,完成处理业务逻辑后,负责将结果写出channel。可用资源池来管理。

经典的架构图如下:

(五)Tomcat架构及运行原理之IO多路复用_第4张图片

三、Tomcat之IO多路复用应用

1.模型流程

Tomcat9所使用的IO多路复用模型可以看成是Reactor、Select和Poll的组合型,此话怎讲?上面画了Select和Reactor的大致模型图,现在来看下Tomcat关于多路复用的模型图:

(五)Tomcat架构及运行原理之IO多路复用_第5张图片

这个模型图一看便可以知道select、poll这两个在Reactor模型中承担了Reactor角色的作用。在tomcat的这个模型中,select的流程有点稍微的区别,原来的select模型是在调用select方法时阻塞,然后处理接收数据就绪的通道。而在tomcat模型中其各自工作流程如下:

  1. 在Acceptor角色中先调用accept方法接收IO请求通道Channel;
  2. 把IO通道放入PollerEvent对象中;
  3. 随后Poller线程中将会遍历PollerEvent数组,将其中的IO通道注册到Selector中;
  4. 使用Selector那套流程再对已经进来的IO请求进行一次多路复用;
  5. 把和SelectionKey关联起来的IO通道传到线程池中的处理线程;
  6. 处理线程对不同的请求类型进行处理返回(如HTTP和WebSocket这种不同的请求类型)。

2.模型初始化源码分析

上面已经描述过大致的模型图和流程,接下来稍微结合Tomcat关于这方便的实现来大致说明Tomcat对于IO多路复用的实现。

2.1 ProtocolHandler之Http11NioProtocol初始化

如果要谈Tomcat的IO多路复用实现,那么模型架构的初始化就不得不说。在我们常用的Springboot中,代码中指定了默认的协议处理器。简易代码如下:

public class TomcatServletWebServerFactory 
        extends AbstractServletWebServerFactory
        implements ConfigurableTomcatWebServerFactory, 
        ResourceLoaderAware {
    public static final String DEFAULT_PROTOCOL = 
            "org.apache.coyote.http11.Http11NioProtocol";
}

因此Http11NioProtocol实现类就是我们分析Tomcat对于IO多路复用模型使用的入口。需要注意的是除了Http11NioProtocol实现类,还有诸如Http11Nio2Protocol、Http11AprProtocol等实现类,但是一般而言我们使用的都是Http11NioProtocol,除非有特殊的需求。

对于每个ProtocolHandler实现类而言,每个实现类都有一套对应的Endpoint、Acceptor等实现类,而对于Http11NioProtocol来说,其对应的实现类如下:

  • Endpoint为NioEndpoint类;
  • Acceptor就是Acceptor类;
  • executor为ThreadPoolExecutor类;
  • ThreadFactory为TaskThreadFactory类;
  • workQueue为TaskQueue类;
  • SocketProcessorBase为NioEndpoint内部类SocketProcessor。

看过Tomcat框架源码的都会知道,Tomcat的关键组件都是有一套生命周期流程的,要看初始化直接从init()或者start()方法开始就行了。因此我们直接看到其抽象实现类AbstractProtocol的init()和start()方法,其关键源码如下:

public abstract class AbstractProtocol implements ProtocolHandler, MBeanRegistration {
    @Override
    public void init() throws Exception {
        if (oname == null) {
            // 创建之后的oname格式为:
            // domain:type=ProtocolHandler,port=port,address=hostAddress
            oname = createObjectName();
            if (oname != null) {
                Registry.getRegistry(null, null).registerComponent(this, 
                        oname, null);
            }
        }
        if (this.domain != null) {
            rgOname = new ObjectName(domain +":type=GlobalRequestProcessor,
                    name=" + getName());
            Registry.getRegistry(null, null).registerComponent(
                    getHandler().getGlobal(), rgOname, null);
        }
        // 在Http11NioProtocol实例化时,就已经实例化了endpoint
        // 类型为NioEndpoint
        // 调用完getName()方法,返回的endpointName格式为:
        // SSL的类型    :https-openssl/jsse-nio[-hostAddress]-port
        // 普通HTTP类型 :http-nio[-hostAddress][-port/auto-N]
        String endpointName = getName();
        endpoint.setName(endpointName.substring(1, 
                endpointName.length()-1));
        endpoint.setDomain(domain);
        // endpoint被初始化
        endpoint.init();
    }
    @Override
    public void start() throws Exception {
        // 调用start方法,并且启动监听
        endpoint.start();
        monitorFuture = getUtilityExecutor().scheduleWithFixedDelay(
                new Runnable() {
                    @Override
                    public void run() {
                        if (!isPaused()) {
                            startAsyncTimeout();
                        }
                    }
                }, 0, 60, TimeUnit.SECONDS);
    }
}

2.2 Endpoint和Accptor初始化

Endpoint的操作是在刚刚的ProtocolHandler中被调用的,分别是init()和start()方法,因此初始化我们就看这两个方法就行了。部分源码如下:

public abstract class AbstractEndpoint {
    public final void init() throws Exception {
        // 主要初始化的便是初始化服务器socket并绑定地址
        if (bindOnInit) {
            // 绑定IP和端口信息,初始化serverSock服务端口对象以及selectorPool
            bindWithCleanup();
            bindState = BindState.BOUND_ON_INIT;
        }
        // 后面的方法可以忽略
    }
    public final void start() throws Exception {
        if (bindState == BindState.UNBOUND) {
            // 绑定IP和端口信息
            bindWithCleanup();
            bindState = BindState.BOUND_ON_START;
        }
        // 开始真正的start方法
        startInternal();
    }
    @Override
    public void startInternal() throws Exception {
        // 确保只会初始化一次
        if (!running) {
            running = true;
            paused = false;
            // processorCache用来缓存SocketProcessorBase类型的对象
            // 避免每次处理一个IO请求都重复创建新的对象,减少创建和销毁对象开销
            if (socketProperties.getProcessorCache() != 0) {
                // SynchronizedStack.DEFAULT_SIZE默认为128
                processorCache = new SynchronizedStack<>(
                        SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getProcessorCache());
            }
            // 每次当有新的请求进来时都会产生一个PollerEvent事件
            // 当有新请求时如果以前的PollerEvent事件有空闲的,则会被拿来重复
            // 使用,同样也是为了减少对象开销
            if (socketProperties.getEventCache() != 0) {
                eventCache = new SynchronizedStack<>(
                        SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getEventCache());
            }
            // 同样的作用,为了减少对象开销
            if (socketProperties.getBufferPool() != 0) {
                nioChannels = new SynchronizedStack<>(
                        SynchronizedStack.DEFAULT_SIZE,
                        socketProperties.getBufferPool());
            }
            // 如果处理线程的工作线程池为空则创建
            if (getExecutor() == null) {
                createExecutor();
            }
            // 初始化connectionLimitLatch,用来控制最大并发连接数
            initializeConnectionLatch();
            // 开始实例化Poller,并且将其设定为保护线程,让其在后台一直运行
            poller = new Poller();
            Thread pollerThread = new Thread(poller, getName() + 
                    "-ClientPoller");
            pollerThread.setPriority(threadPriority);
            pollerThread.setDaemon(true);
            pollerThread.start();
            // 开始初始化Accptor对象
            startAcceptorThread();
        }
    }
    protected void startAcceptorThread() {
        // 同样的,实例化Acceptor对象,并设定为保护线程,让其一直在后台运行
        acceptor = new Acceptor<>(this);
        String threadName = getName() + "-Acceptor";
        acceptor.setThreadName(threadName);
        Thread t = new Thread(acceptor, threadName);
        t.setPriority(getAcceptorThreadPriority());
        t.setDaemon(getDaemon());
        t.start();
    }
}

就这么几段代码,将接下来要分析的Tomcat IO多路复用关键组件全都初始化了。

3.模型运行源码分析

模型已经初始化完成,接下来粗略的看下Tomcat的IO多路复用组件是如何搭配运行的。

注:接下来的源码分析只会贴出一些对于流程特别重要的代码,对于和数据流向无关的代码忽略,并且只看正常流程,受制于篇幅异常流程暂不分析。

3.1 Accptor接收器

这个组件的作用便是用来接收新的IO请求,并将其封装成Channel,随后传给Reacto处理。由于Acceptor是一个线程实现类,因此运行时只需要从run()方法开始看起即可,其关键源码如下:

public class Acceptor implements Runnable {
    @Override
    public void run() {
        // 如果endpoint没有被关闭,则无线循环下去
        while (endpoint.isRunning()) {
            // 状态控制忽略
            ...
            try {
                // 在这里控制最大的连接数,如果框架内的连接数已达maxConnections
                // 则在这里暂停,等待有socket被关闭或者销毁
                // 需要注意的是maxConnections默认值是10000,即意味着本服务器
                // 同时最多只能有10000个套接字同时运行被Acceptor这套体系管理
                // 如果并发量超过了10000则不会再创建新的套接字,直到有新的套接字
                // 在这个流程中被关闭或者销毁
                endpoint.countUpOrAwaitConnection();
                // 如果暂停则不再接收新的套接字请求
                if (endpoint.isPaused()) {
                    continue;
                }
                U socket = null;
                try {
                    // 接收下一个从ServerSocketChannel对象中接收到的请求
                    socket = endpoint.serverSocketAccept();
                } catch (Exception ioe) {
                    // 如果没有获取到socket则让阈值-1
                    endpoint.countDownConnection();
                    // 略
                    ...
                }
                ...
                // 开始处理Socket套接字
                if (endpoint.isRunning() && !endpoint.isPaused()) {
                    // 如果setSocketOptions()处理成功则会返回true,失败
                    // 则会关闭套接字
                    if (!endpoint.setSocketOptions(socket)) {
                        endpoint.closeSocket(socket);
                    }
                } else {
                    // 如果没有运行或者暂停了则销毁套接字
                    endpoint.destroySocket(socket);
                }
            } catch (Throwable t) {
                ...
            }
        }
        state = AcceptorState.ENDED;
    }
}

Acceptor的作用实际上就是不断调用endpoint来获取Socket套接字,然后再调用endpoint中的方法来处理套接字,同时实现一个控制maxConnections参数的功能。满足Reactor所定义的用来处理客户端新连接,并将请求分发到处理器中。

3.2 Reactor容器之Endpoint

这个类的功能定位便是连接Poller和Acceptor,相当于是一个切入点或者管理类,毕竟Poller所执行的流程都在NioEndpoint中进行。其接收Socket套接字和将Socket注册到Poller的关键源码如下:

public class NioEndpoint 
        extends AbstractJsseEndpoint {
    // NIO中负责接收客户端请求的服务类
    private volatile ServerSocketChannel serverSock = null;
    @Override
    protected SocketChannel serverSocketAccept() throws Exception {
        // 接收时这里面的方法会阻塞,直到有新的连接就绪并复制成功才会返回
        // 具体的细节有兴趣可以看下流程,但这里对于其它的类的流程不做过多分析
        return serverSock.accept();
    }
    @Override
    protected boolean setSocketOptions(SocketChannel socket) {
        // 开始执行连接
        try {
            // 将socket的阻塞状态关闭
            socket.configureBlocking(false);
            Socket sock = socket.socket();
            socketProperties.setProperties(sock);
            // 将Socket封装成NioChannel
            NioChannel channel = null;
            if (nioChannels != null) {
                // 试着使用已经缓存的nioChannel通道
                channel = nioChannels.pop();
            }
            // 如果没有缓存了,则新创建封装一个
            if (channel == null) {
                SocketBufferHandler bufhandler = new SocketBufferHandler(
                        socketProperties.getAppReadBufSize(),
                        socketProperties.getAppWriteBufSize(),
                        socketProperties.getDirectBuffer());
                if (isSSLEnabled()) {
                    channel = new SecureNioChannel(socket, bufhandler, 
                            selectorPool, this);
                } else {
                    // 一般而言会创建这种类型
                    channel = new NioChannel(socket, bufhandler);
                }
            } else {
                // 如果使用缓存成功,则更新socket,且将对象中的动态数据重置
                channel.setIOChannel(socket);
                channel.reset();
            }
            // 进一步封装Channel对象,后续使用的对象类型都是这种类型的
            NioSocketWrapper socketWrapper = new NioSocketWrapper(channel, 
                    this);
            channel.setSocketWrapper(socketWrapper);
            socketWrapper.setReadTimeout(getConnectionTimeout());
            socketWrapper.setWriteTimeout(getConnectionTimeout());
            socketWrapper.setKeepAliveLeft(NioEndpoint.this
                    .getMaxKeepAliveRequests());
            socketWrapper.setSecure(isSSLEnabled());
            // 注册到Poller对象中,开始进行IO多路复用,随后的主角便是Poller
            poller.register(channel, socketWrapper);
            // 注册成功后返回true
            return true;
        } catch (Throwable t) {
            ...
        }
        return false;
    }
}

对于Endpoint而言,在IO多路复用中其实际上更像一个容器,里面包含了Poller和PollerEvent等对象,并且在Endpoint中实现整个Reactor的角色职能。

3.3 处理IO多路复用的Poller和PollerEvent

从上面的逻辑延续下来,我们暂时可以知道接下来应该需要看三个方法:一个是register,另外两个分别是Poller和PollerEvent的run()方法。先看到Poller的相关关键源码:

public class Poller implements Runnable {
    public void register(final NioChannel socket, 
            final NioSocketWrapper socketWrapper) {
        // 这是OP_REGISTER变成的,对应着OP_REGISTER,标志进行read操作
        socketWrapper.interestOps(SelectionKey.OP_READ);
        PollerEvent r = null;
        // 试着从缓存中拿取到空闲对象
        if (eventCache != null) {
            r = eventCache.pop();
        }
        // 如果没有空闲对象则实例化一个,有的话则重置内部动态属性
        // 且将intOps设置成OP_REGISTER,后续会用到
        if (r == null) {
            r = new PollerEvent(socket, OP_REGISTER);
        } else {
            r.reset(socket, OP_REGISTER);
        }
        // 将获得的PollerEvent事件添加到events数组中
        addEvent(r);
    }
    private void addEvent(PollerEvent event) {
        // 添加数据到数组
        events.offer(event);
        // 设置信号量,说明已经有新的请求事件发生,这里后续会说明
        if (wakeupCounter.incrementAndGet() == 0) {
            selector.wakeup();
        }
    }
    @Override
    public void run() {
        // 一直轮询直到destroy()方法被调用
        while (true) {
            boolean hasEvents = false;
            try {
                if (!close) {
                    // 没有关闭,先调用events方法,这个方法的作用便是异步唤醒
                    // 所有的PollerEvent
                    hasEvents = events();
                    // 设置wakeupCounter为-1,代表selector正在阻塞,等待
                    // 新的请求进来
                    if (wakeupCounter.getAndSet(-1) > 0) {
                        // wakeupCounter原来的值如果大于0,代表有请求事件
                        // 无需等待,直接立刻获取即可
                        keyCount = selector.selectNow();
                    } else {
                        // 如果进到这里面说明当前没有请求事件,需要阻塞等待
                        // 有新的请求事件发生
                        keyCount = selector.select(selectorTimeout);
                    }
                    // 获取完之后重新归0
                    wakeupCounter.set(0);
                }
                if (close) {
                    // 如果已经关闭,也要依次异步唤醒PollerEvent事件,在里面
                    // 对其进行相应的处理
                    events();
                    timeout(0, false);
                    try {
                        // 关闭slector复用器
                        selector.close();
                    } catch (IOException ioe) {
                    }
                    break;
                }
            } catch (Throwable x) {
                continue;
            }
            // 跑到这里keyCount为0说明selector复用器没有获取到请求
            // 不管是因为超时还是wakeup导致的,如果hasEvents为false都先
            // 唤醒一遍全部的PollerEvent事件
            if (keyCount == 0) {
                hasEvents = (hasEvents | events());
            }
            // 如果有请求事件,则将selector的所有已就绪selectionKey遍历一遍
            Iterator iterator =
                keyCount > 0 ? selector.selectedKeys().iterator() : null;
            while (iterator != null && iterator.hasNext()) {
                // 获取SelectionKey和刚在外部封装的NioSocketWrapper对象
                SelectionKey sk = iterator.next();
                NioSocketWrapper socketWrapper = 
                        (NioSocketWrapper) sk.attachment();
                if (socketWrapper == null) {
                    // 为空则删除
                    iterator.remove();
                } else {
                    // 先删除,再调用processKey()方法,processKey方法如果调用
                    // 正常的话最终将会调用到processSocket()方法中
                    iterator.remove();
                    processKey(sk, socketWrapper);
                }
            }
            // 处理超时情况,如果没有事件则将注册到selector的selectionKey取消
            // 取消后会调用socketWrapper的close方法、SelectionKey的cancel方法
            // 并将SelectionKey关联的Channel通道也关闭
            timeout(keyCount,hasEvents);
        }
        getStopLatch().countDown();
    }
    public boolean processSocket(SocketWrapperBase socketWrapper,
        SocketEvent event, boolean dispatch) {
        // processKey()方法是判断具体要使用什么方法来处理这次请求
        // 但如果都正常的话最终都会调用到这个方法中来,并且processKey()
        // 方法大多是当前流程的额外处理逻辑,对整个的流程来说无影响,不在
        // 本次的分析范围内,略过
        try {
            // socketWrapper不能为空
            if (socketWrapper == null) {
                return false;
            }
            // SocketProcessorBase对象就算是Reactor模型中的请求线程了
            // 可以看成是HandlerThread,最终对请求的处理将会在这个线程中完成
            SocketProcessorBase sc = null;
            if (processorCache != null) {
                // 如果缓存中有可用对象则优先使用缓存对象
                sc = processorCache.pop();
            }
            if (sc == null) {
                // 没有缓存对象则创建新的SocketProcessorBase对象
                sc = createSocketProcessor(socketWrapper, event);
            } else {
                // 用的是缓存中的则重置动态数据
                sc.reset(socketWrapper, event);
            }
            Executor executor = getExecutor();
            // 如果是Reactor多线程类型的,则使用多线程模型来处理请求线程
            // 如果是Reactor单线程的则使用单请求线程来处理
            if (dispatch && executor != null) {
                // Reactor多线程
                executor.execute(sc);
            } else {
                // Reactor单线程
                sc.run();
            }
        } catch (RejectedExecutionException ree) {
            ...
            return false;
        } 
        return true;
    }
}

对于Poller中的addEvent()方法那里为什么要设置wakeupCounter这个值来判断呢?实际上变量wakeupCounter的值相当于一个信号,初始值为0, 当wakeupCounter为0时代表selector没有调用select方法阻塞自己,当wakeupCounter为-1时代表selector正在执行select方法阻塞,等待有新的套接字事件发生,直到超时。因此这里的调用incrementAndGet方法判断是否为0则意味着有新的请求事件发生,需要唤醒selector,毕竟selector是在守护程中一直阻塞的。

Poller中的方法有点多,但毕竟Reactor组件是负责分发派送请求的,因此处理的逻辑多也是没有办法的事。

上面提到过PollerEvent,但方法并未贴出来,接下来看下PollerEvent的关键方法源码:

public class Poller implements Runnable {
    public boolean events() {
        // 这个方法其实很简单,只是单纯的遍历一遍events的EventPoller事件对象
        // 对调用其run()方法,随后重置EventPoller属性,并加入到eventCache
        // 缓存中
        boolean result = false;
        PollerEvent pe = null;
        // 开始遍历,遍历完之后events对象将会为空
        for (int i = 0, size = events.size(); i < size 
                && (pe = events.poll()) != null; i++ ) {
            result = true;
            try {
                // 调用EventPoller的run()方法,但是直接调用线程对象的run()
                // 方法确实少见。。
                pe.run();
                // 重置EventPoller数据
                pe.reset();
                if (running && !paused && eventCache != null) {
                    // 满足下次利用条件则放入到事件缓存对象中
                    eventCache.push(pe);
                }
            } catch ( Throwable x ) {
                ...
            }
        }
        return result;
    }
}
public static class PollerEvent implements Runnable {
    private NioChannel socket;
    private int interestOps;
    @Override
    public void run() {
        // 这个方法会在Poller调用events()方法后被调用
        if (interestOps == OP_REGISTER) {
            // 如果PollerEvent操作类型为OP_REGISTER,则说明这个PollerEvent
            // 事件是刚刚添加进来的尚未注册,这里将会把socket注册到selector
            // 中,以便后续selector调用select方法获取到
            try {
                socket.getIOChannel().register(socket.getSocketWrapper()
                        .getPoller().getSelector(), SelectionKey.OP_READ, 
                                socket.getSocketWrapper());
            } catch (Exception x) {
                ...
            }
        } else {
            // 如果是其它的操作类型则先获取socket绑定的SelectionKey
            final SelectionKey key = socket.getIOChannel()
                    .keyFor(socket.getSocketWrapper().getPoller()
                            .getSelector());
            try {
                if (key == null) {
                    try {
                        // 如果key为空,说明绑定失败,直接调用close关闭连接
                        socket.socketWrapper.close();
                    } catch (Exception ignore) {
                        ...
                    }
                } else {
                    final NioSocketWrapper socketWrapper = 
                            (NioSocketWrapper) key.attachment();
                    if (socketWrapper != null) {
                        // 一切正常,则将socketWrapper和key都设置成
                        // interestOps的操作类型,如read或者write
                        int ops = key.interestOps() | interestOps;
                        socketWrapper.interestOps(ops);
                        key.interestOps(ops);
                    } else {
                        // 如果实际的socketWrapper对象为空,则取消当前key
                        socket.getSocketWrapper().getPoller()
                            .cancelledKey(key, socket.getSocketWrapper());
                    }
                }
            } catch (CancelledKeyException ckx) {
                try {
                    // 发生了异常则取消当前key
                    socket.getSocketWrapper().getPoller()
                            .cancelledKey(key, socket.getSocketWrapper());
                } catch (Exception ignore) {}
            }
        }
    }
}

到这里关于Reactor便已经分析完成,想要了解Acceptor、Poller和PollerEvent这三个关键组件以及和NIO的Selector是如何搭配合作以达到高吞吐量的性能,光是看一遍文章或者笔记是远远不够的,必须还得知道NIO的运作机制以及亲身去分析一遍Tomcat的IO多路复用实现,这样才能够对Tomcat的实现有更清晰的认识。

3.4 执行用户请求的线程对象SocketProcessorBase

这一部分便是和Handler相关的处理线程了,在这个线程中将会使用Handler来处理具体的用户请求。实际上SocketProcessorBase实现了Runnable,如果Reactor组件没有配置线程池,那么请求只会有一个SocketProcessorBase线程来处理,即Reactor单线程模型;如果配置了线程池,才是Reactor多线程模型。接下来简略的看下其执行流程:

public abstract class SocketProcessorBase implements Runnable {
    protected SocketWrapperBase socketWrapper;
    protected SocketEvent event;
    @Override
    public final void run() {
        synchronized (socketWrapper) {
            // socketWrapper可能会是read和write模式,添加synchronized锁
            // 是为了不让这两种模式同时进行
            if (socketWrapper.isClosed()) {
                return;
            }
            doRun();
        }
    }
    protected abstract void doRun();
}

父类很简单,直接调用了子类的doRun()方法。因此直接看到子类doRun()方法的流程:

protected class SocketProcessor extends SocketProcessorBase {
    @Override
    protected void doRun() {
        // 获取实际的socket对象和关联的SelectionKey对象
        NioChannel socket = socketWrapper.getSocket();
        SelectionKey key = socket.getIOChannel()
            .keyFor(socket.getSocketWrapper().getPoller().getSelector());
        Poller poller = NioEndpoint.this.poller;
        if (poller == null) {
            socketWrapper.close();
            return;
        }
        try {
            int handshake = -1;
            // 这里会有一段代码,用来判断三次握手,不是本次的主流程,略过
            if (handshake == 0) {
                SocketState state = SocketState.OPEN;
                // 在线程中使用Handler对象来处理请求
                if (event == null) {
                    state = getHandler().process(socketWrapper, 
                            SocketEvent.OPEN_READ);
                } else {
                    state = getHandler().process(socketWrapper, event);
                }
            } 
            // 握手失败的情况略过
            ...
        } // 异常情况也忽略
        ...
         finally {
            socketWrapper = null;
            event = null;
            // 如果Reactor模型正常运行,则将当前线程对象添加到processorCache
            // 缓存中,以便后续使用
            if (running && !paused && processorCache != null) {
                processorCache.push(this);
            }
        }
    }
}

至此,源码层面的分析流程到此结束。

 

 

你可能感兴趣的:(#,Tomcat,虚拟机相关,tomcat,IO多路复用,Reactor模型,epoll,多线程)