Tomcat 连接器组件及IO模型

Tomcat作为一个Web服务器, 必然需要一个组件,这个组件可以解析http请求, 并封装响应对象, 返回给请求客户端. 而连接器就是这样的组件. 连接器是一个独立的模块,它可以插入到容器中,为容器的invoke方法提供必要参数
在这里插入图片描述
它会将接收到的请求解析为Request和Response对象. Tomcat的连接器有很多种,但它们毫无例外都实现了Connect接口, 这个接口声明了四个很重要的方法:
在这里插入图片描述
这四个方法的作用分别是获取和设置连接器所属的容器, 以及创建请求和响应对象.下面来分析该组件是如何解析请求的

LimitLatch

为了防止访问请求的流量过多而把服务器冲垮, 我们需要采用流量控制保护机制, 控制套接字的连接数。Tomcat通过连接数控制器LimitLatch来达到这一目的。Tomcat 的流量控制器是通过 AQS 并发框架来实现的,通过 AQS 实现起来更具灵活性和定制性。Tomcat引入了AtomicLong作为计数变量, 思路是先初始化同步器最大限制值,然后每接收一个套接字就将计数变量累加1,每关闭一个套接字将计数变量减 1,如此一来, 一旦计数变量值大于最大限制值,则 AQS机制将会将接收线程阻塞,而停止对套接字的接收,直到某些套接字处理完关闭后重新唤起接收线程往下接收套接字。

/**
 * Shared latch that allows the latch to be acquired a limited number of times
 * after which all subsequent requests to acquire the latch will be placed in a
 * FIFO queue until one of the shares is returned.
 */
public class LimitLatch {

    private class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected int tryAcquireShared(int ignored) {
            long newCount = count.incrementAndGet();
            if (newCount > limit) {
                // Limit exceeded
                count.decrementAndGet();
                return -1;
            } else {
                return 1;
            }
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            count.decrementAndGet();
            return true;
        }
    }

    private final Sync sync;
    private final AtomicLong count;
    private volatile long limit;


    /**
     * Acquires a shared latch if one is available or waits for one if no shared
     * latch is current available.
     * @throws InterruptedException If the current thread is interrupted
     */
    public void countUpOrAwait() throws InterruptedException {
        sync.acquireSharedInterruptibly(1);
    }

    /**
     * Releases a shared latch, making it available for another thread to use.
     * @return the previous counter value
     */
    public long countDown() {
        sync.releaseShared(0);
        long result = getCount();
        return result;
    }
}

我们使用LimitLatch的方式可以用如下伪代码表示

LimitLatch limitLatch = new LimitLatch(200); 
ServerSocket serverSocket;
While(true) { 
	limitLatch.countUpOrAwait() ; 
	Socket socket= serverSocket.accept() ;
	in another thread {
		process(socket);
		limitLatch.countDown();
	}
} 

Tomcat的阻塞式IO模型

以HttpConnector为例, 它创建ServerSocket套接字, 调用accept方法,为每一个连接单独分配一个线程进行处理. 伪代码如下

while(!stop){
	Socket socket = serverSocket.accept();
	HttpProcessor processor = createProcessor();
	processor.assign(socket);
}

assign()方法是非阻塞的, 它会为该socket分配一个线程,然后立即返回,以便Connector线程可以接受下一个请求.至于对该socket连接的解析,则放在HttpProcessor线程内进行.createProcessor方法是从线程池中弹出一个空闲的线程. HttpProcessor类实现了Runnable接口,它的run方法如下:
在这里插入图片描述
可以看到, 它不断的等待一个socket的到来,如果没有给它分配socket, 他就会在await()方法上阻塞, 如果有socket, 就会拿到该socket并调用process方法解析. 那么, 它是如何知道connector为它分配了一个socket的呢? 我们知道,当connector调用它的assign方法时, 就为它分配了一个socket, 也就是说, assign方法负责唤醒await方法

在这里插入图片描述
assign方法为该processor分配一个socket, 即this.socket = socket,随后调用notifyAll唤醒await的wait阻塞, 而await从wait醒来之后, 用一个临时变量接受了这个socket, 即 Socket socket = this. socket, 然后告诉assign方法,它可以继续接受下一个socket了, 然后将socket返回. 这里为什么要用临时变量接受而不立即返回呢? 因为一旦调用了notifyAll,那么assign就可能醒过来, 并为this.socket分配一个新的连接, 我们会将这个新的连接返回而忽略了之前的连接.
然后, process方法会调用parseConnection, parseRequest和parseHeaders方法开始解析引入的Http请求.
可以看到, 上述的模型是一个接收线程负责接收请求, 而线程池中包含多个处理任务的线程, 是一个基本的阻塞式IO模型。
在这里插入图片描述

Tomcat NIO模型

在这里插入图片描述

NIO的控制阀门的大小比BIO的更大, BIO 模式受本身模式的限制,它
的连接数与线程数比例是 1:1 关系,即一个线程负责处理一个连接。所以当连接数太多时将导致线程数也很多,JVM线程数过多将导致线程间切换成本很高。默认情况下, Tomcat 处理连接池的线程数为 200 ,所以 BIO流量控制阀门大小也默认设置为 200。但 NIO 模式能克服 BIO 连接数的不足,它能基于事件同时维护大量的连接,对于事件的遍历只须交给同一个或少量的线程,再把具体的事件执行逻辑交给线程池。例如, Tomcat 套接字接收工作交给一个线程,而把套接字读写及处理工作交给N个线程, N一般为 CPU 核数。对于 NIO 模式,Tomcat 默认把流量阀门大小设置为 10000 , 如果你想更改大小,可以通过 server.xm 中<Connector>节点的 maxConnections 属性修改,同时要注意,连接数到达最大值后,操作系统仍然会接收客户端连接,直到操作系统接收队列被塞满。 队列默认长度为 100 ,可通过 server.xml Connector>节点的 acceptCount 属性配置。

Acceptor工作流程

NioEndpoint的启动方法会启动一个Acceptor线程, Acceptor接收SocketChannel后对其进行简单处理, 将其封装为NioChannel对象和NioSocketWrapper对象, 并将其注册到通道队列中, 并由Poller负责轮询。随后, Acceptor就可以进行下一个连接的接收操作。NioChannel中多出来了JVM层面的读写缓冲区。NioChannel属于频繁生成与消除的对象,因为每个客户端连接都需要一个通道与之相对 ,频繁地生成和消除在性能的损耗上也不得不多加考虑,我们需要一种手段规避此处可能带来的性能问题。其思想就是当某个客户端使用完 NioChannel 对象后,不对其进行回收,而是将它缓存起来,当新客户端访问到来时,只需替换其中的SocketChannel 对象即可, NioChannel 对象包含的其他属性只须做重置操作即可,如此一来就不必频繁生成与消除 NioChannel 对象
在这里插入图片描述

在这里插入图片描述

Poller的run方法是一个死循环, 该循环首先调用events()方法, events()方法从PollerEvent队列中取出PollerEvent, 然后根据事件类型进行不同处理, 如果是注册事件, 则将该事件关联的channel注册到selector上, 如果是读/写事件, 则说明selector上此时已经注册过了channel, 只需在原有的感兴趣事件集中添加读/写事件的标志位即可。执行完events方法后, Poller还会调用select方法, 得到SelectionKey集合,该集合就是那些需要被处理的key, 然后调用processKey, processKey会用该key的socket和对应的状态创建SocketProcessor对象, 并将其丢入线程池中, 由线程池完成socket的读写操作。Poller由一个Poller池维护, 这样可以使用多个线程进行轮询工作
在这里插入图片描述
SocketProcessor会对可读取状态套接字的字节流进行读取, 解析http报文,组装Request和Reponse对象, 并通过Adapter的service方法将这两个对象传给Service组件, Service组件关联了多个Connector, 负责解析不同的协议, 同时关联了一个容器, Service组件收到Connector传来的两个对象时, 会将这两个对象交由Service关联的容器进行处理, 该容器一般为Engine

最后, 连接器如何与具体的容器关联呢? 在tomcat启动类上面, 我们可以看到这种关联 :
在这里插入图片描述
一旦connector的init和start方法被调用, 这个连接器就可以处理http请求了.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值