一个应用服务器的性能很大程度上取决于网络通信模块的实现,因此Connector对于tomcat而言是重中之重。在本章节中以下两个问题会被回答:
- 一个http请求是怎么被tomcat监听到的,会有哪些处理;
- 为什么请求可以有需要通过nginx的,也可以不需要nginx的直接请求到tomcat上?
1 Connector配置
通过对Container的初始化分析,我们很自然的会回过头看conf/server.xml中的connector配置。在xml中配置了2个connector。
<Connector port="8080" protocol="HTTP/1.1"
connectionTimeout="20000"
redirectPort="8443" />
<Connector port="8009" protocol="AJP/1.3" redirectPort="8443" />
为什么会有多个Connector呢?我们部署服务器的时候,通常会有2种方式:
- 1 直接部署tomcat,在浏览器中请求http与tomcat直连
- 2 部署一个nginx作反向代理,tomcat与nginx直连
这就是上面两种配置,通过协议protocol来区分。所以多个connector的好处是通过不同的协议,是tomcat服务器能够随着http的应用场景,服务器架构的升级而兼容起来。
好的,现在配置了2个Connector,那么继续思考一下,Connector是通信过程,如果是你你会怎么设计?显然需要做3件事:
- (1)监听端口,创建服务端与客户端的链接;
- (2)获取到客户端请求的socket数据,并对Socket数据进行解析和包装成Http请求数据格式;
- (3)将包装后的数据交给Container处理。
通过源码来分析,Connector有两个属性:protocolHandler(协议)和adapter(适配器),其中protocolHandler完成的是步骤(1)(2),adapter完成的是步骤(3)。
2 初始化工作
2.1 Connector构造函数
在Connector的构造方法中,通过反射生成protocolHandler.
public Connector(String protocol) {
setProtocol(protocol);
// Instantiate protocol handler
try {
Class<?> clazz = Class.forName(protocolHandlerClassName);
this.protocolHandler = (ProtocolHandler) clazz.newInstance();
} catch (Exception e) {
log.error(sm.getString(
"coyoteConnector.protocolHandlerInstantiationFailed"), e);
}
}
协议的设置在conf/server.xml中配置,由setProtocol来赋值,Tomcat提供了6种协议:
三种不带Ajp的协议,客户端与Tomcat服务器直接连接。
Http11Protocol---------------默认协议,阻塞IO方式的支持http1.1协议
Http11NioProtocol----------非阻塞IO方式的支持http1.1协议
Http11AprProtocol----------使用ARP技术的支持http1.1协议(ARP:Apache portable runtime)
三种带Ajp的协议为定向包协议,即WEB服务器通过 TCP连接和SERVLET容器连接,例如tomcat和Apache、Nginx等前端服务器连接
AjpProtocol--------------------阻塞IO方式的支持Ajp协议
AjpNioProtocol---------------非阻塞IO方式的支持Ajp协议
AjpAprProtocol---------------使用ARP技术的支持Ajp协议
为了便于分析,之对Http11Protocol进行分析,其他的大同小异。在Http11Protocol的构造方法中,对成员变量endpoint和cHandler进行初始化,这两个很重要,后续会继续讲到。
继续到Connector代码中,由前面提到的tomcat启动过程知道,会调用Connector两个方法init和start。而端口的绑定和监听则分别在这两个方法中完成
2.2 Connector的init方法
调用的是initInternal,做了三件事:
- 1 适配器初始化成CoyoteAdapter;
- 2 调用Http11Protocol的init方法;
- 3 调用mapperListener的init方法。
protected void initInternal() throws LifecycleException {
super.initInternal();
//TODO:注意,是在在这里初始化的Adapter,将是配置赋值给协议处理类
adapter = new CoyoteAdapter(this);
protocolHandler.setAdapter(adapter);
……
try {
//TODO:在这里初始化http11protocol
protocolHandler.init();
} catch (Exception e) {
……
}
……
mapperListener.init();
}
步骤2中Http11Protocol的init方法,最终会调用到其父类AbstractProtocol的init方法,在这个方法里面对endpoint(Http11Protocol使用的是JIoEndPoint)进行了初始化。
public void init() throws Exception {
……
try {
//endpoint进行了初始化。
endpoint.init();
} catch (Exception ex) {
……
}
}
endpoint.init()在AbstractEndpoint中,完成了对需要监听的端口的绑定。
public final void init() throws Exception {
testServerCipherSuitesOrderSupport();
if (bindOnInit) {
bind();//绑定服务端需要监听的端口
bindState = BindState.BOUND_ON_INIT;
}
}
在JIoEndpoint的bind()中完成了对端口的绑定。
2.3 Connector的start方法
Connector的启动会调用start方法,在startInternal方法中,
protected void startInternal() throws LifecycleException {
……
setState(LifecycleState.STARTING);//发送STARTING事件
try {
protocolHandler.start();//启动端口监听
}
……
mapperListener.start();//很重要,后面会提到
}
其中,protocolHandler.start();即调用了Http11Protocol的start方法。最终调用了调用了JIoEndpoint的startInternal方法,初始化了连接请求处理的线程池,监听端口的线程(默认200个)
public void startInternal() throws Exception {
if (!running) {
……
if (getExecutor() == null) {
createExecutor();//初始化请求处理的线程池
}
initializeConnectionLatch();//设置链接线程阈值
startAcceptorThreads();//初始化监听接收客户端请求的线程
……
}
}
OK,到了现在Connector的启动已经透明化了,Connector的初始化工作其实是根据server.xml的配置,创建了服务端Socket来监听客户端的请求。并初始化了一个线程池来处理接收到的请求。
通过上面的分析,我们可以看到JIoEndpoint是一个阻塞式的IO模型;而若使用Http11NioProtocol协议,则调用的是NioEndpoint,是一个多路复用IO模型。
系列文章直达:
初始化与启动:https://yq.aliyun.com/articles/20169?spm=0.0.0.0.4yGfpo
容器:https://yq.aliyun.com/articles/20172?spm=0.0.0.0.2uPEZi
连接器:https://yq.aliyun.com/articles/20175?spm=0.0.0.0.2uPEZi
一个http请求的经历:https://yq.aliyun.com/articles/20177?spm=0.0.0.0.2uPEZi
重要的设计模式:https://yq.aliyun.com/articles/20179?spm=0.0.0.0.2uPEZi