SpringBoot Tomcat(1) Http11NioProtocol模型之NioEndpoint

2,073 阅读3分钟

说明:本系列的tomcat为spring boot内置,版本为9.0,使用Http11NioProtocol协议。

jar包信息:

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-tomcat</artifactId>
        <version>2.1.1.RELEASE</version>
    </dependency>  

Http11NioProtocol表示非阻塞模式的HTTP协议的通信,在启动tomcat的过程中,会初始化NioEndpoint这个类,它包含了很多子类,下面将对重点类介绍。

Acceptor

Acceptor主要用来监听是否有客户端连接进来并接收连接,它是一个线程类。

Acceptor->run():
    while (endpoint.isRunning()) {
    ......
        if (!endpoint.isRunning()) {
            break;
        }
        // endpoint内部封装LimitLatch对象,默认10000个连接,调用下面的方法后,连接数+1,当达到上限,会阻塞
        endpoint.countUpOrAwaitConnection();
    ......
        // 启动阶段设置为阻塞模式,因此该方法会一直阻塞,直到有客户端连接,返回的是SocketChannel
        // 内部调用serverSock.accept()方法,serverSock是在NioEndpoint->initServerSocket()方法中初始化的
        socket = endpoint.serverSocketAccept();
    ......
        // 设置通道的属性
        if (!endpoint.setSocketOptions(socket)) {
            // 调用此方法后,连接数-1
            endpoint.closeSocket(socket);
        }

这里的接收器是一个循环,只有当不再连接的时候才会退出

NioEndpoint->setSocketOptions():
    // 设置为非阻塞的原因是后面对客户端所有的连接都采取非阻塞模式
    socket.configureBlocking(false);
    ......
    // 从nioChannels栈中取出最后一个
    NioChannel channel = nioChannels.pop();
    if (channel == null) {
        ......
    else {
        channel.setIOChannel(socket);
        channel.reset();
    }
    getPoller0().register(channel);

从栈中取出channel的时候会判断,如果为空则新建一个,否则将相应的属性替换和重置,这是因为NioChannel属于频繁生成与消除的对象,只替换属性可以极大地节省JVM创建和回收整个对象,从而优化性能。当事件终止后,会调用close()方法,这时nioChannels会将socket压入栈。nioChannels默认最大是500个,达到上限则不压栈,而是调用socket.free()方法。

Poller

getPoller0()方法获得Poller类,正如其名,Poller负责轮训事件栈,首先是在Acceptor中调用事件注册方法。

NioEndpoint->Poller->register():
    PollerEvent r = eventCache.pop();
    // 从eventCache弹栈,同样的为空创建非空重置
    ......
    // 压入events栈
    addEvent(r);
NioEndpoint->Poller->run():
    // 此线程无限循环,直到destroy()被调用
    while (true) {
        if (!close) {
            // 判断events栈顶是否有事件	
            hasEvents = events();
            // 定义了两个Poller类,所以会存在返回结果大于0的情况
            if (wakeupCounter.getAndSet(-1) > 0) {
                keyCount = selector.selectNow();
            } else {
                keyCount = selector.select(selectorTimeout);
            }
            // 设置为0,所以上面返回的结果可能会大于0
            wakeupCounter.set(0);
        }
        Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;
        while (iterator != null && iterator.hasNext()) {
            SelectionKey sk = iterator.next();
            NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
            if (attachment == null) {
                iterator.remove();
            } else {
                iterator.remove();
                // 处理程序
                processKey(sk, attachment);
            }
        }
        timeout(keyCount,hasEvents);
    }
NioEndpoint->Poller->processKey():
    if (sk.isReadable()) {
        if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
            closeSocket = true;
        }
    }

关于events()方法的具体作用可以点击这里

Executor

Executor是任务执行器,用来处理请求任务。SocketProcessorBase是任务定义器,负责定义任务的执行逻辑,它是抽象类,实现类为NioEndpoint的内部类SocketProcessor

AbstractEndpoint->processSocket():
    // processorCache弹栈获取任务定义器SocketProcessorBase,同样的为空创建非空重置
    SocketProcessorBase<S> sc = processorCache.pop();
    ......
    // 获取任务线程池,如果有线程池则放入线程池执行,否则自己执行
    Executor executor = getExecutor();
    if (dispatch && executor != null) {
        executor.execute(sc);
    } else {
        sc.run();
    }

启动的时候会创建任务线程池

AbstractEndpoint->createExecutor():
    internalExecutor = true;
    TaskQueue taskqueue = new TaskQueue();
    TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
    executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
    taskqueue.setParent( (ThreadPoolExecutor) executor);

根据线程池定义可知任务执行线程池默认的属性为:

  • 核心线程数10个
  • 最大线程数200个
  • 线程名为http-nio-8080-exec-%d形式
  • 阻塞队列为先进先出模式,数量为Integer的最大值,也就是小规模并发数基本不可能使用到最大线程数

小结

至此发现,Http11NioProtocol模型中

  1. 用了大量的while(true)循环
  2. tomcat对请求的处理最终是在NioEndpoint类的Executor执行的,这也就是为什么执行到我们业务逻辑的线程名总是http-nio-8080-exec-%d的原因了
  3. 为了节省对象创建与销毁的开销,使用了为空创建非空重置属性的策略