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请求了.