注:本文源码分析基于 tomcat 9.0.43,源码的gitee仓库仓库地址:gitee.com/funcy/tomca….
本文是tomcat源码分析的第四篇,本文来分析Connector的启动流程。
1. Connector的创建
在我们的示例demo中,我们是这样创建Connector的:
public class Demo01 {
@Test
public void test() throws Exception {
Tomcat tomcat = new Tomcat();
// 创建连接器
Connector connector = new Connector();
connector.setPort(8080);
connector.setURIEncoding("UTF-8");
tomcat.getService().addConnector(connector);
...
}
}
进入Connector的构造方法:
public Connector() {
this("HTTP/1.1");
}
tomcat默认提供的参数为HTTP/1.1,这个参数在ProtocolHandler#create方法中会用到:
也就是说,tomcat默认创建的ProtocolHandler为org.apache.coyote.http11.Http11NioProtocol。
我们再看看Http11NioProtocol的构造方法:
public Http11NioProtocol() {
super(new NioEndpoint());
}
这里我们又看到一个关键组件:NioEndpoint,Http11NioProtocol中使用的Endpoint为NioEndpoint。
到此,简单的一个Connector的创建为我们准备了三个组件:
Connector:连接器Http11NioProtocol:http协议的nio实现NioEndpoint:NIO量身定制的线程池
关于NioEndpoint,其注释如下:
NIO tailored thread pool, providing the following services:
- Socket acceptor thread
- Socket poller thread
- Worker threads pool
NIO量身定制的线程池,提供以下服务
- socket 连接线程(处理socket连接事件)
- socket 轮询线程(处理socket读写事件)
- 工作线程
这三个组件的具体功能我们后面再分析。
2. Connector 初始化
接关我们来看看Connector的初始化,进入Connector#initInternal:
protected void initInternal() throws LifecycleException {
// 省略了一些代码
...
try {
protocolHandler.init();
} catch (Exception e) {
...
}
}
Connector#initInternal方法中,做的主要工作就是调用protocolHandler.init()方法了,我们进入AbstractProtocol#init一探究竟:
public void init() throws Exception {
// 设置 endpoint 的一些属性
String endpointName = getName();
endpoint.setName(endpointName.substring(1, endpointName.length()-1));
endpoint.setDomain(domain);
// 绑定端口、主机,设置 bindState,处理了oname属性值
endpoint.init();
}
这块设置了endpoint的一些属性,然后调用endpoint.init()方法,根据前面的分析,这里的endpoint类型是NioEndpoint,我们进入AbstractEndpoint#init
public final void init() throws Exception {
if (bindOnInit) {
// 绑定端口、主机
bindWithCleanup();
bindState = BindState.BOUND_ON_INIT;
}
...
}
在AbstractEndpoint#init中,主要调用了bindWithCleanup(),该方法就是用来处理ip、端口绑定的,跟着这个方法执行下去,最终执行到NioEndpoint#bind方法:
public void bind() throws Exception {
// 初始化serverSocket
initServerSocket();
setStopLatch(new CountDownLatch(1));
initialiseSsl();
// 开启 selector
selectorPool.open(getName());
}
这个方法先是初始化了serverSocket,然后开启了一个selector,我们且看下去:
2.1 NioEndpoint#initServerSocket
NioEndpoint#initServerSocket代码如下:
protected void initServerSocket() throws Exception {
if (...) {
...
} else {
// 得到一个 ServerSocketChannel
serverSock = ServerSocketChannel.open();
socketProperties.setProperties(serverSock.socket());
// 获取地址与端口
InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());
// 进行绑定操作
serverSock.bind(addr, getAcceptCount());
}
// 设置true,表示还是阻塞的
serverSock.configureBlocking(true);
}
先获取了一个ServerSocketChannel,然后为其绑定ip与端口,这些都是很标准的nio操作了,没啥好说的,不过令人疑惑的是,在最后调用的serverSock.configureBlocking(...)方法:
serverSock.configureBlocking(true);
传入的值为true,这表示ServerSocketChannel是阻塞的!这表示,tomcat在处理连接请求时,使用的阻塞IO !
2.2 selectorPool.open(...)
我们再来看看selectorPool.open(...)的执行,进入NioSelectorPool#open方法:
public void open(String name) throws IOException {
enabled = true;
// 获取 selector
getSharedSelector();
if (shared) {
blockingSelector = new NioBlockingSelector();
// 开启 selector
blockingSelector.open(name, getSharedSelector());
}
}
我们先来看看 NioSelectorPool#getSharedSelector 方法:
protected Selector getSharedSelector() throws IOException {
if (shared && sharedSelector == null) {
synchronized (NioSelectorPool.class) {
if (sharedSelector == null) {
// 得到一个 selector 对象,nio 提供的方法
sharedSelector = Selector.open();
}
}
}
return sharedSelector;
}
这个方法就是用来产生一个selector对象的,没干什么重要的事。
我们继续,进入NioBlockingSelector#open方法:
public void open(String name, Selector selector) {
sharedSelector = selector;
poller = new BlockPoller();
poller.selector = sharedSelector;
poller.setDaemon(true);
poller.setName(name + "-BlockPoller");
poller.start();
}
这个方法创建了一个BlockPoller实例,处理了一些属性后,调用了BlockPoller#start方法,我们进入BlockPoller:
protected static class BlockPoller extends Thread {
/**
* Thread#run 方法
*/
@Override
public void run() {
while (run) {
try {
events();
int keyCount = 0;
try {
int i = wakeupCounter.get();
// 进行 selector 操作
if (i > 0) {
keyCount = selector.selectNow();
} else {
wakeupCounter.set(-1);
keyCount = selector.select(1000);
}
}
// 省略一些代码
...
}
// 省略了一些配置
...
}
}
}
BlockPoller 继承了Threade,在它的run()方法中,进行了selector.select(...)操作,这些都是nio的标准操作了。
如果有小伙伴对nio编程比较熟悉,就会发现到目前为止,这个selector上没有绑定ServerSocketChannel,也没有注册任何要轮询的事件,因此selector.selectNow()/selector.select(...)方法不会获取到任何channel,关于它的作用,我们后面再分析。
到这里,Connector 的初始化操作就完成了,主要进行了如下操作:
- 初始化
ServerSockChannel(只是创建了对象,绑定ip、端口,并未对外监听连接) - 开启
BlockPoller线程(selector没有绑定ServerSocketChannel,也没注册要轮询的事件)
3. Connector 启动
梳理完初始化流程后,接着我们来看看启动流程:Connector#startInternal:
protected void startInternal() throws LifecycleException {
// 省略了一些代码
...
try {
protocolHandler.start();
} catch (Exception e) {
...
}
}
进入 AbstractEndpoint#start 方法:
public final void start() throws Exception {
if (bindState == BindState.UNBOUND) {
// 绑定端口、主机
bindWithCleanup();
bindState = BindState.BOUND_ON_START;
}
startInternal();
}
初始操作已经执行过了,代码会直接运行startInternal(),也就是NioEndpoint#startInternal方法:
@Override
public void startInternal() throws Exception {
if (!running) {
running = true;
paused = false;
...
// 创建处理业务逻辑的线程池
if (getExecutor() == null) {
createExecutor();
}
// 初始化连接数计算器,就是用来计算服务端连接数的
initializeConnectionLatch();
// 设置pollerThread相关信息
poller = new Poller();
Thread pollerThread = new Thread(poller, getName() + "-ClientPoller");
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
// 启动 Poller 线程,处理读写事件
pollerThread.start();
// 启动 Acceptor 线程,处理连接事件
startAcceptorThread();
}
}
NioEndpoint#startInternal方法的流程如下:
- 创建了一个线程池,该线程池主要是用来处理业务的
- 初始化连接数计算器,tomcat可以配置最大连接请求,就是在这里计算当前连接请求的
- 启动
Poller线程,处理读写事件 - 启动
Acceptor线程,处理连接事件
3.1 启动Poller线程
我们先来看看Poller:
public class Poller implements Runnable {
private Selector selector;
public Poller() throws IOException {
// 得到一个 selector 对象
this.selector = Selector.open();
}
@Override
public void run() {
while (true) {
boolean hasEvents = false;
try {
if (!close) {
hasEvents = events();
// 1. selector 操作
if (wakeupCounter.getAndSet(-1) > 0) {
keyCount = selector.selectNow();
} else {
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);
}
...
if (keyCount == 0) {
hasEvents = (hasEvents | events());
}
} catch (Throwable x) {
...
}
Iterator<SelectionKey> iterator =
keyCount > 0 ? selector.selectedKeys().iterator() : null;
// selector 上存在key,进入处理流程
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
if (socketWrapper != null) {
// 2. 处理请求
processKey(sk, socketWrapper);
}
}
}
...
}
...
}
Poller实现了Runnable接口,在构造方法中,会得到一个selector对象,紧接着就在run()方法中使用这个selector对象了,同BlockPoller, 这个selector上没有绑定ServerSocketChannel,也没有注册任何要轮询的事件.
接着我们再看Poller#run()方法,这个方法的轮询操作BlockPoller相差无几,纵观整个方法,就只干了两件事:
- 轮询请求
- 调用
processKey(...)处理请求
selector上没有绑定ServerSocketChannel,也没有注册任何要轮询的事件,因此根本不会有请求过来,那这个轮询有何意义,processKey(...)又是处理什么请求呢?这两个疑问先留着,我们在下篇分析tomcat处理请求连接时再揭晓吧!
3.2 启动Acceptor线程
我们再看看Acceptor线程的启动,进入AbstractEndpoint#startAcceptorThread方法:
protected void startAcceptorThread() {
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();
}
这个方法先是创建了Acceptor实例,传入的参数为this,其实就是NioEndpoint,接着就是设置了一些属性,然后启动了线程,我们进入Acceptor:
public class Acceptor<U> implements Runnable {
/** 类型是 NioEndpoint */
private final AbstractEndpoint<?,U> endpoint;
...
public Acceptor(AbstractEndpoint<?,U> endpoint) {
this.endpoint = endpoint;
}
@Override
public void run() {
int errorDelay = 0;
try {
while (!stopCalled) {
...
try {
...
U socket = null;
try {
// 接收服务端连接
socket = endpoint.serverSocketAccept();
} catch (Exception ioe) {
endpoint.countDownConnection();
if (endpoint.isRunning()) {
errorDelay = handleExceptionWithDelay(errorDelay);
throw ioe;
} else {
break;
}
}
errorDelay = 0;
if (!stopCalled && !endpoint.isPaused()) {
// 手动注册到 selector
if (!endpoint.setSocketOptions(socket)) {
endpoint.closeSocket(socket);
}
} else {
endpoint.destroySocket(socket);
}
} catch (Throwable t) {
...
}
}
} finally {
stopLatch.countDown();
}
state = AcceptorState.ENDED;
}
...
}
在Acceptor#run()方法中,会调用endpoint.serverSocketAccept()获取服务端的连接,然后调用endpoint.setSocketOptions(...)将连接注册到selector上。
我们来看看它是怎么获取到一个连接的,进入NioEndpoint#serverSocketAccept方法:
protected SocketChannel serverSocketAccept() throws Exception {
return serverSock.accept();
}
这里调用的是ServerSocketChannel#accept方法来获取服务端连接,需要注意的是,这个方法是阻塞的,也就是说,tomcat会一直在这等着,直到有新的服务端连接进来才会被唤醒,因此Acceptor是阻塞的。
总结下就是,tomcat在获取服务端连接时,使用的是阻塞IO!
到这里,Contector的启动就结果了,endpoint.serverSocketAccept()方法调用后,就能监听连接请求了。
4. 总结
本文分析了Connector的启动流程,分为两个部分来分析:初始化与启动,总结如下:
- 初始化:创建了
ServerSocketChannel,并绑定了IP与端口;启动了BlockPoller线程 - 启动:启动了
Poller线程与Acceptor线程
需要注意的是:
tomcat在获取服务端连接时,使用的是阻塞IO- 到目前为止,
Poller线程的selector没有绑定ServerSocketChannel,也没有注册任何要轮询的事件
好了,本文就到这里,我们下一篇再来分析tomcat的连接处理过程。
限于作者个人水平,文中难免有错误之处,欢迎指正!原创不易,商业转载请联系作者获得授权,非商业转载请注明出处。
本文首发于微信公众号 Java技术探秘,链接:mp.weixin.qq.com/s/XB0nKcbw-…,如果您喜欢本文,欢迎关注该公众号,让我们一起在技术的世界里探秘吧!