tomcat源码_Tomcat 中的 NIO 源码分析

之前写了两篇关于 NIO 的文章,第一篇介绍了Java NIO:Buffer、Channel 和 Selector使用,第二篇Java 非阻塞 IO 和异步 IO,并展示了简单的用例。

本文将介绍 Tomcat 中的 NIO 使用,使大家对 Java NIO 的生产使用有更加直观的认识。

虽然本文的源码篇幅也不短,但是 Tomcat 的源码毕竟不像 Doug Lea 的并发源码那么“变态”,对于大部分读者来说,阅读难度比之前介绍的其他并发源码要简单一些,所以读者不要觉得有什么压力。

本文基于 Tomcat 当前(2018-03-20)最新版本 9.0.6。

先简单画一张图示意一下本文的主要内容:

c34bd6fd3471cb593ebb7d639b93dcb0.png

目录

源码环境准备

由于上面下载的 tomcat 的源码并没有使用 maven 进行组织,不方便我们看源码,也不方便我们进行调试。这里我们将使用 maven 仓库中的 tomcat-embed-core,自己编写代码进行启动的方式来进行调试。

首先,创建一个空的 maven 工程,然后添加以下依赖。

<dependency>
<groupId>org.apache.tomcat.embedgroupId>
<artifactId>tomcat-embed-coreartifactId>
<version>9.0.6version>
dependency>
上面的依赖,只会将 tomcat-embed-core-9.0.6.jar 和 tomcat-annotations-api-9.0.6.jar 两个包引进来,对于本文来说,已经足够了,如果你需要其他功能,需要额外引用其他的依赖,如 Jasper。

然后,使用以下启动方法:

public static void main(String[] args) throws LifecycleException {

Tomcat tomcat = new Tomcat;

Connector connector = new Connector("HTTP/1.1");
connector.setPort(8080);
tomcat.setConnector(connector);

tomcat.start;
tomcat.getServer.await;
}

经过以上的代码,我们的 Tomcat 就启动起来了。

Tomcat 中的其他接口感兴趣的读者请自行探索,如设置 webapp 目录,设置 resources 等

这里,介绍第一个重要的概念:Connector。在 Tomcat 中,使用 Connector 来处理连接,一个 Tomcat 可以配置多个 Connector,分别用于监听不同端口,或处理不同协议。

在 Connector 的构造方法中,我们可以传 HTTP/1.1AJP/1.3用于指定协议,也可以传入相应的协议处理类,毕竟协议不是重点,将不同端口进来的连接对应不同处理类才是正道。典型地,我们可以指定以下几个协议处理类:

  • org.apache.coyote.http11.Http11NioProtocol:对应非阻塞 IO

  • org.apache.coyote.http11.Http11Nio2Protocol:对应异步 IO

  • org.apache.coyote.http2.Http2Protocol:对应 http2 协议,对 http2 感兴趣的读者,赶紧看起来吧。

本文的重点当然是非阻塞 IO 了,之前已经介绍过异步 IO的基础知识了,读者看完本文后,如果对异步 IO 的处理流程感兴趣,可以自行去分析一遍。

如果你使用 9.0 以前的版本,Tomcat 在启动的时候是会自动配置一个 connector 的,我们可以不用显示配置。

9.0 版本的 Tomcat#start 方法:

public void start throws LifecycleException {
getServer;
server.start;
}

8.5 及之前版本的 Tomcat#start 方法:

public void start throws LifecycleException {
getServer;
// 自动配置一个使用非阻塞 IO 的 connector
getConnector;
server.start;
}

endpoint

前面我们说过一个 Connector 对应一个协议,当然这描述也不太对,NIO 和 NIO2 就都是处理 HTTP/1.1 的,只不过一个使用非阻塞,一个使用异步。进到指定 protocol 代码,我们就会发现,它们的代码及其简单,只不过是指定了特定的 endpoint。

打开 Http11NioProtocolHttp11Nio2Protocol源码,我们可以看到,在构造方法中,它们分别指定了 NioEndpoint 和 Nio2Endpoint。

// 非阻塞模式
public classHttp11NioProtocolextendsAbstractHttp11JsseProtocol<NioChannel> {
publicHttp11NioProtocol {
// NioEndpoint
super(new NioEndpoint);
}
...
}
// 异步模式
public classHttp11Nio2ProtocolextendsAbstractHttp11JsseProtocol<Nio2Channel> {

publicHttp11Nio2Protocol {
// Nio2Endpoint
super(new Nio2Endpoint);
}
...
}

这里介绍第二个重要的概念:endpoint。Tomcat 使用不同的 endpoint 来处理不同的协议请求,今天我们的重点是 NioEndpoint,其使用非阻塞 IO 来进行处理 HTTP/1.1 协议的请求。

NioEndpoint 继承 => AbstractJsseEndpoint 继承 => AbstractEndpoint。中间的 AbstractJsseEndpoint 主要是提供了一些关于 HTTPS的方法,这块我们暂时忽略它,后面所有关于 HTTPS 的我们都直接忽略,感兴趣的读者请自行分析。

init 过程分析

下面,我们看看从 tomcat.start 一直到 NioEndpoint 的过程。

1. AbstractProtocol # init

@Override
public voidinit throws Exception {
...
String endpointName = getName;
endpoint.setName(endpointName.substring(, endpointName.length-1 1));
endpoint.setDomain(domain);
// endpoint 的 name=http-nio-8089,domain=Tomcat
endpoint.init;
}

2. AbstractEndpoint # init

public final void init throws Exception {
if (bindOnInit) {
bind; // 这里对应的当然是子类 NioEndpoint 的 bind 方法
bindState = BindState.BOUND_ON_INIT;
}
...
}

3. NioEndpoint # bind

这里就到我们的 NioEndpoint 了,要使用到我们之前学习的 NIO 的知识了。

@Override
public voidbind throws Exception {
// initServerSocket; 原代码是这行,我们 “内联” 过来一起说

// 开启 ServerSocketChannel
serverSock = ServerSocketChannel.open;
socketProperties.setProperties(serverSock.socket);

// getPort 会返回我们最开始设置的 8080,得到我们的 address 是 0.0.0.0:8080
InetSocketAddress addr = (getAddress!=?new InetSocketAddress(getAddress,getPort):new InetSocketAddress(getPort));

// ServerSocketChannel 绑定地址、端口,
// 第二个参数 backlog 默认为 100,超过 100 的时候,新连接会被拒绝(不过源码注释也说了,这个值的真实语义取决于具体实现)
serverSock.socket.bind(addr,getAcceptCount);

// ※※※ 设置 ServerSocketChannel 为阻塞模式 ※※※
serverSock.configureBlocking(true);

// 设置 acceptor 和 poller 的数量,至于它们是什么角色,待会说
// acceptorThreadCount 默认为 1
if (acceptorThreadCount == 0) {
// FIXME: Doesn't seem to work that well with multiple accept threads
// 作者想表达的意思应该是:使用多个 acceptor 线程并不见得性能会更好
acceptorThreadCount = 1;
}

// poller 线程数,默认值定义如下,所以在多核模式下,默认为 2
// pollerThreadCount = Math.min(2,Runtime.getRuntime.availableProcessors);
if (pollerThreadCount <= 0) {
pollerThreadCount = 1;
}

//
setStopLatch(new CountDownLatch(pollerThreadCount));

// 初始化 ssl,我们忽略 ssl
initialiseSsl;

// 打开 NioSelectorPool,先忽略它
selectorPool.open;
}
  1. ServerSocketChannel 已经打开,并且绑定要了之前指定的 8080 端口,设置成了阻塞模式。

  2. 设置了 acceptor 的线程数为 1

  3. 设置了 poller 的线程数,单核 CPU 为 1,多核为 2

  4. 打开了一个 SelectorPool,我们先忽略这个

到这里,我们还不知道 Acceptor 和 Poller 是什么东西,我们只是设置了它们的数量,我们先来看看最后面提到的 SelectorPool。

start 过程分析

刚刚我们分析完了 init 过程,下面是启动过程 start 分析。

AbstractProtocol # start

@Override
public voidstart throws Exception {
...
// 调用 endpoint 的 start 方法
endpoint.start;

// Start async timeout thread
asyncTimeout = new AsyncTimeout;
Thread timeoutThread = new Thread(asyncTimeout, getNameInternal + "-AsyncTimeout");
int priority = endpoint.getThreadPriority;
if (priority < Thread.MIN_PRIORITY || priority > Thread.MAX_PRIORITY) {
priority = Thread.NORM_PRIORITY;
}
timeoutThread.setPriority(priority);
timeoutThread.setDaemon(true);
timeoutThread.start;
}

AbstractEndpoint # start

public final void start throws Exception {
// 按照我们的流程,刚刚 init 的时候,已经把 bindState 改为 BindState.BOUND_ON_INIT 了,
// 所以下面的 if 分支我们就不进去了
if (bindState == BindState.UNBOUND) {
bind;
bindState = BindState.BOUND_ON_START;
}
// 往里看 NioEndpoint 的实现
startInternal;
}

下面这个方法还是比较重要的,这里会创建前面说过的 acceptor 和 poller。

NioEndpoint # startInternal

@Override
public voidstartInternal throws Exception {

if (!running) {
running = true;
paused = false;

// 以下几个是缓存用的,之后我们也会看到很多这样的代码,为了减少 new 很多对象出来
processorCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getProcessorCache);
eventCache = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getEventCache);
nioChannels = new SynchronizedStack<>(SynchronizedStack.DEFAULT_SIZE,
socketProperties.getBufferPool);

// 创建【工作线程池】,Tomcat 自己包装了一下 ThreadPoolExecutor,
// 1. 为了在创建线程池以后,先启动 corePoolSize 个线程(这个属于线程池的知识了,不熟悉的读者可以看看我之前的文章)
// 2. 自己管理线程池的增长方式(默认 corePoolSize 10, maxPoolSize 200),不是本文重点,不分析
if ( getExecutor == ) {
createExecutor;
}

// 设置一个栅栏(tomcat 自定义了类 LimitLatch),控制最大的连接数,默认是 10000
initializeConnectionLatch;

// 开启 poller 线程
// 还记得之前 init 的时候,默认地设置了 poller 的数量为 2,所以这里启动 2 个 poller 线程
pollers = new Poller[getPollerThreadCount()];
for (int i=0; ipollers[i] = new Poller;
Thread pollerThread = new Thread(pollers[i], getName + "-ClientPoller-"+i);
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);
pollerThread.start;
}
// 开启 acceptor 线程,和开启 poller 线程组差不多。
// init 的时候,默认地,acceptor 的线程数是 1
startAcceptorThreads;
}
}

到这里,我们启动了工作线程池、 poller 线程组、acceptor 线程组。同时,工作线程池初始就已经启动了 10 个线程。我们用 jconsole 来看看此时的线程,请看下图:

3d2de3fbac46be31bdd9eca1ae352003.png

从 jconsole 中,我们可以看到,此时启动了 BlockPoller、worker、poller、acceptor、AsyncTimeout,大家应该都已经清楚了每个线程是哪里启动的吧。

Tomcat 中并没有 Worker 这个类,此名字是我瞎编。

此时,我们还是不知道 acceptor、poller 甚至 worker 到底是干嘛的,下面,我们从 acceptor 线程开始看起。

Acceptor

它的结构非常简单,在构造函数中,已经把 endpoint 传进来了,此外就只有 threadName 和 state 两个简单的属性。

private final AbstractEndpoint,U> endpoint;
private String threadName;
protected volatile AcceptorState state = AcceptorState.NEW;

publicAcceptor(AbstractEndpoint,U> endpoint) {
this.endpoint = endpoint;
}

threadName 就是一个线程名字而已,Acceptor 的状态 state 主要是随着 endpoint 来的。

public enum AcceptorState {
NEW, RUNNING, PAUSED, ENDED
}

我们直接来看 acceptor 的 run 方法吧:

Acceptor # run

@Override
public voidrun {

int errorDelay = 0;

// 只要 endpoint 处于 running,这里就一直循环
while (endpoint.isRunning) {

// 如果 endpoint 处于 pause 状态,这边 Acceptor 用一个 while 循环将自己也挂起
while (endpoint.isPaused && endpoint.isRunning) {
state = AcceptorState.PAUSED;
try {
Thread.sleep(50);
} catch (InterruptedException e) {
// Ignore
}
}
// endpoint 结束了,Acceptor 自然也要结束嘛
if (!endpoint.isRunning) {
break;
}
state = AcceptorState.RUNNING;

try {
// 如果此时达到了最大连接数(之前我们说过,默认是10000),就等待
endpoint.countUpOrAwaitConnection;

// Endpoint might have been paused while waiting for latch
// If that is the case, don't accept new connections
if (endpoint.isPaused) {
continue;
}

U socket = ;
try {
// 这里就是接收下一个进来的 SocketChannel
// 之前我们设置了 ServerSocketChannel 为阻塞模式,所以这边的 accept 是阻塞的
socket = endpoint.serverSocketAccept;
} catch (Exception ioe) {
// We didn't get a socket
endpoint.countDownConnection;
if (endpoint.isRunning) {
// Introduce delay if necessary
errorDelay = handleExceptionWithDelay(errorDelay);
// re-throw
throw ioe;
} else {
break;
}
}
// accept 成功,将 errorDelay 设置为 0
errorDelay = 0;

if (endpoint.isRunning && !endpoint.isPaused) {
// setSocketOptions 是这里的关键方法,也就是说前面千辛万苦都是为了能到这里进行处理
if (!endpoint.setSocketOptions(socket)) {
// 如果上面的方法返回 false,关闭 SocketChannel
endpoint.closeSocket(socket);
}
} else {
// 由于 endpoint 不 running 了,或者处于 pause 了,将此 SocketChannel 关闭
endpoint.destroySocket(socket);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
String msg = sm.getString("endpoint.accept.fail");
// APR specific.
// Could push this down but not sure it is worth the trouble.
if (t instanceof Error) {
Error e = (Error) t;
if (e.getError == 233) {
// Not an error on HP-UX so log as a warning
// so it can be filtered out on that platform
// See bug 50273
log.warn(msg, t);
} else {
log.error(msg, t);
}
} else {
log.error(msg, t);
}
}
}
state = AcceptorState.ENDED;
}

大家应该发现了,Acceptor 绕来绕去,都是在调用 NioEndpoint 的方法,我们简单分析一下这个。

在 NioEndpoint init 的时候,我们开启了一个 ServerSocketChannel,后来 start 的时候,我们开启多个 acceptor(实际上,默认是 1 个),每个 acceptor 启动以后就开始循环调用 ServerSocketChannel 的 accept 方法获取新的连接,然后调用 endpoint.setSocketOptions(socket) 处理新的连接,之后再进入循环 accept 下一个连接。

到这里,大家应该也就知道了,为什么这个叫 acceptor 了吧?接下来,我们来看看 setSocketOptions 方法到底做了什么。

NioEndpoint # setSocketOptions

@Override
protected booleansetSocketOptions(SocketChannel socket) {
try {
// 设置该 SocketChannel 为非阻塞模式
socket.configureBlocking(false);
Socket sock = socket.socket;
// 设置 socket 的一些属性
socketProperties.setProperties(sock);

// 还记得 startInternal 的时候,说过了 nioChannels 是缓存用的。
// 限于篇幅,这里的 NioChannel 就不展开了,它包括了 socket 和 buffer
NioChannel channel = nioChannels.pop;
if (channel == ) {
// 主要是创建读和写的两个 buffer,默认地,读和写 buffer 都是 8192 字节,8k
SocketBufferHandler bufhandler = new SocketBufferHandler(
socketProperties.getAppReadBufSize,
socketProperties.getAppWriteBufSize,
socketProperties.getDirectBuffer);
if (isSSLEnabled) {
channel = new SecureNioChannel(socket, bufhandler, selectorPool, this);
} else {
channel = new NioChannel(socket, bufhandler);
}
} else {
channel.setIOChannel(socket);
channel.reset;
}

// getPoller0 会选取所有 poller 中的一个 poller
getPoller0.register(channel);
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
try {
log.error(
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值