说明:本系列的tomcat为spring boot内置,版本为9.0,使用Http11NioProtocol协议。
jar包信息:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<version>2.1.1.RELEASE</version>
</dependency>
Http11NioProtocol表示非阻塞模式的HTTP协议的通信,在启动tomcat的过程中,会初始化NioEndpoint这个类,它包含了很多子类,下面将对重点类介绍。
Acceptor
Acceptor主要用来监听是否有客户端连接进来并接收连接,它是一个线程类。
Acceptor->run():
while (endpoint.isRunning()) {
......
if (!endpoint.isRunning()) {
break;
}
// endpoint内部封装LimitLatch对象,默认10000个连接,调用下面的方法后,连接数+1,当达到上限,会阻塞
endpoint.countUpOrAwaitConnection();
......
// 启动阶段设置为阻塞模式,因此该方法会一直阻塞,直到有客户端连接,返回的是SocketChannel
// 内部调用serverSock.accept()方法,serverSock是在NioEndpoint->initServerSocket()方法中初始化的
socket = endpoint.serverSocketAccept();
......
// 设置通道的属性
if (!endpoint.setSocketOptions(socket)) {
// 调用此方法后,连接数-1
endpoint.closeSocket(socket);
}
这里的接收器是一个循环,只有当不再连接的时候才会退出
NioEndpoint->setSocketOptions():
// 设置为非阻塞的原因是后面对客户端所有的连接都采取非阻塞模式
socket.configureBlocking(false);
......
// 从nioChannels栈中取出最后一个
NioChannel channel = nioChannels.pop();
if (channel == null) {
......
else {
channel.setIOChannel(socket);
channel.reset();
}
getPoller0().register(channel);
从栈中取出channel的时候会判断,如果为空则新建一个,否则将相应的属性替换和重置,这是因为NioChannel属于频繁生成与消除的对象,只替换属性可以极大地节省JVM创建和回收整个对象,从而优化性能。当事件终止后,会调用close()
方法,这时nioChannels会将socket压入栈。nioChannels默认最大是500个,达到上限则不压栈,而是调用socket.free()
方法。
Poller
getPoller0()
方法获得Poller类,正如其名,Poller负责轮训事件栈,首先是在Acceptor中调用事件注册方法。
NioEndpoint->Poller->register():
PollerEvent r = eventCache.pop();
// 从eventCache弹栈,同样的为空创建非空重置
......
// 压入events栈
addEvent(r);
NioEndpoint->Poller->run():
// 此线程无限循环,直到destroy()被调用
while (true) {
if (!close) {
// 判断events栈顶是否有事件
hasEvents = events();
// 定义了两个Poller类,所以会存在返回结果大于0的情况
if (wakeupCounter.getAndSet(-1) > 0) {
keyCount = selector.selectNow();
} else {
keyCount = selector.select(selectorTimeout);
}
// 设置为0,所以上面返回的结果可能会大于0
wakeupCounter.set(0);
}
Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
NioSocketWrapper attachment = (NioSocketWrapper)sk.attachment();
if (attachment == null) {
iterator.remove();
} else {
iterator.remove();
// 处理程序
processKey(sk, attachment);
}
}
timeout(keyCount,hasEvents);
}
NioEndpoint->Poller->processKey():
if (sk.isReadable()) {
if (!processSocket(attachment, SocketEvent.OPEN_READ, true)) {
closeSocket = true;
}
}
关于events()
方法的具体作用可以点击这里
Executor
Executor是任务执行器,用来处理请求任务。SocketProcessorBase是任务定义器,负责定义任务的执行逻辑,它是抽象类,实现类为NioEndpoint
的内部类SocketProcessor
。
AbstractEndpoint->processSocket():
// processorCache弹栈获取任务定义器SocketProcessorBase,同样的为空创建非空重置
SocketProcessorBase<S> sc = processorCache.pop();
......
// 获取任务线程池,如果有线程池则放入线程池执行,否则自己执行
Executor executor = getExecutor();
if (dispatch && executor != null) {
executor.execute(sc);
} else {
sc.run();
}
启动的时候会创建任务线程池
AbstractEndpoint->createExecutor():
internalExecutor = true;
TaskQueue taskqueue = new TaskQueue();
TaskThreadFactory tf = new TaskThreadFactory(getName() + "-exec-", daemon, getThreadPriority());
executor = new ThreadPoolExecutor(getMinSpareThreads(), getMaxThreads(), 60, TimeUnit.SECONDS,taskqueue, tf);
taskqueue.setParent( (ThreadPoolExecutor) executor);
根据线程池定义可知任务执行线程池默认的属性为:
- 核心线程数10个
- 最大线程数200个
- 线程名为http-nio-8080-exec-%d形式
- 阻塞队列为先进先出模式,数量为Integer的最大值,也就是小规模并发数基本不可能使用到最大线程数
小结
至此发现,Http11NioProtocol模型中
- 用了大量的
while(true)
循环 - tomcat对请求的处理最终是在
NioEndpoint
类的Executor
执行的,这也就是为什么执行到我们业务逻辑的线程名总是http-nio-8080-exec-%d的原因了 - 为了节省对象创建与销毁的开销,使用了为空创建非空重置属性的策略