Tomcat 网络实现分析
网络请求流程图解:
www.processon.com/view/link/6…
核心类
网络模型
springboot 中的内嵌tomcat 默认采用Nio模式, 具体实现:NioEndpoint, Nio2Endpoint 为异步IO的实现。
Nio 网络核心代码:
org.apache.tomcat.util.net.NioEndpoint#startInternal
首先会创建一个线程池,用来处理网络请求任务,该线程池不同于JDK默认线程池。
核心 --> 最大线程 ---> 队列(无界)
然后创建一个acceptor 线程用来接受网络连接事件,一个poller 线程用来处理 read、write事件
acceptor中的ServerSocketChannel 对象初始化:
org.apache.tomcat.util.net.NioEndpoint#initServerSocket:
这里设置为阻塞模式, 不需要使用Selector来监听,因为tomcat中主要还是IO吧,连接数不会太多。
(而Netty默认为非阻塞模式,会注册到一个selector上,为了支持多个ServerSocketChannel对象同时监听连接事件)
当acceptor 接受到一个连接时,会将连接对象包装为一个NioSocketWrapper对象 注册到poller的events中
poller:
这里有一个events() 方法,会遍历events 数组,如果是register类型,会注册该连接对象到Selector中,同时绑定READ事件
处理事件时,将会创建SocketProcessorBase 任务, 提交到线程池执行。
总结: NioEndpoint采用 一个线程处理accept, 一个线程处理Selector上的read、write事件。当read、write事件发生时,交给线程池进行处理。
NIO事件转换:
基本HTTP 请求 事件处理如下:
客户端连接建立
↓
OP_ACCEPT (accept 新连接,添加任务到events)
↓
OP_READ (遍历 events,将其注册OP_READ)
↓
开始处理OP_READ事件 (Poller#run --> processKey)
↓
处理事件开始 (processKey: unreg(), 取消 所有事件: 即之前的OP_READ )
↓
业务逻辑处理 (ConnectionHandler#process)
↓
处理完毕( 上面正常返回OPEN状态(异步也是),执行registerReadInterest,从新向events注册OP_READ的EVENT对象)
↓
Poller继续监听事件
网络请求流程跟踪
当浏览器发起一个URL请求时,tomcat 是如何一步一步进入到Servlet 中的?
整理流程图如下:
首先发起连接后,poller 会监听到相关read事件。
首先取消感兴趣的事件。防止干扰其他线程socket
这里提交SocketProcessorBase对象,即一个任务。包含了当前channel attach的NioSocketWrapper对象信息
Poller#timeout
poller线程在处理事件时,会执行timeout 检查是否有超时的事件。
注意: 在任务处理前,会将相关的事件取消掉,完成后会重新注册相关事件。因此在任务执行过程中,是不会检查超时的。(异步任务检查除外)
超时时间更新:
这里有一个keptAlive参数:首次进来为false,当解析出一个完成的HTTP请求后,执行业务结束后,会赋值为true,在第二次调用parseRequestLine时没有读取到任务数据时,就会将timeout 的事件设置为keepAliveTimeout,即该连接维持的最长时间。
org.apache.coyote.http11.Http11InputBuffer#parseRequestLine
线程池执行doRun:
getHandler: 即NioEndpoint对象初始化时创建的ConnnectionHandler:
创建Http11Processor, 执行process
当上面正常执行结束后,会返回OPEN,会重新向poller 注册事件,用于检查超时。
Http11Processor extends AbstractProcessorLight:
Http11Processor#service方法:
- 首先会解析 request header
- 调用CoyoteAdapter#service
这里的while 通常会执行两次,第一次解析出一个完整的对象后,会向下传递最终执行Servlet。执行完毕后这里会继续尝试解析,如果没有数据后才会退出该while。
-
- parseRequestLine 这里会设置超时参数,用于poller检查连接超时。
- parseRequestLine 这里会设置超时参数,用于poller检查连接超时。
getContainer: 当前为 Engine对象, Engine 中pipeline默认只有一个basic的
engine中继续获取host执行:
执行Context pipeline:
ContextValve
Servlet入口核心类:
分配Servlet对象
过滤出满足当前URL的filter:
执行filter链
doFilter:
filter 执行完后,执行service方法:
Servlet异步分析
通常使用如下:
可以手动调用AsyncContext#setTimeout设置异步超时时间。
异步上下文使用状态机进行管理状态信息(AsyncStateMachine),整体状态流转: DISPATCHED 为AsyncStateMachine对象默认状态
--> STARTING --> STARTED --> COMPLETING --> DISPATCHED
如果没有调用complete方法(Spring 返回callable), 状态可能为:STARTED --> DISPATCHING --> DISPATCHED
- startAsync() 方法 将当前request对象开启异步支持
Request#startAsync: 会创建AsyncContext对象用来管理异步上下文信息, startAsync() 方法执行结束后为STARTING
- 当Controller中返回后,最终执行下面逻辑。
继续返回:
异步将会返回LONG:
- 这里会修改状态为STARTED
对于异步这里会将processor加入到 等待线程池队列,Http11Processor 包含了SocketWrapperBase 任务对象
异步超时检测
processors 最终在这里处理,没1s中检查一次是否超时, 默认30s ,Connector#asyncTimeout,如果超时将会发布timeout事件到线程池, 会自动关闭异步上下文对象,同时执行AsyncContext对象中的listener。
- 调用complete() 方法会修改状态为 COMPLETING , 这里会发布事件任务到线程池(并不是poller 监听的读写事件)
- 由于上面发布了read 事件任务到线程池, 线程池执行该任务最终到:
如果没有执行Complete方法,在执行到AbstractProcessorLight#process时:
- 首先status为OPEN_READ, 会往Servlet 流程执行
- 下面逻辑是一个while , 异步在后面会更新status,下一次进入下面红框的逻辑。
上面while 为true,继续循环, 将会执行到下面ASYNC_END 条件成立分支:这里有两个方法
- dispatch:如果request 处于 dispatching 状态,会继续向Servlet 调用。Spring MVC 中返回callable。
-
- dispatch 里面也可能会触发执行asyncContext对象 listener
- checkForPipelinedData:检查channel是否还有数据没有读取完 。 可能会抛出异常。正常情况返回OPEN,异常则为CLOSE。 这里有一个keptAlive参数,会修改超时时间。
debug时由于超时,socket关闭,因此返回-1,没有数据返回0,不会抛出异常
正常情况下,上面返回0。返回到checkForPipelinedData,最终为OPEN 或 CLOSE(连接被关闭), while循环结束,线程结束。
总结:
当开启异步上下文,从Servlet返回后,tomcat线程会向注册一个事件到waitingProcessors中。退出tomcat线程
内部线程(catalina 线程)会遍历waitingProcessors,检查是否达到异步超时时间,达到则会向线程池 注册一个timeout的event。使tomcat线程 关闭异步上下文
在asynContext调用complete时,会重新发布READ事件任务到tomcat线程池 继续处理异步上下文的剩余过程。
连接关闭
自动检测到连接对象超时:
上面返回CLOSE后,关闭相关的selectionKey
Spring MVC 中的异步
这里使用callable 作为返回值进行分析: WebAsyncTask、CompletableFuture、DeferredResult 类似
异步上下文不同于上面Servlet的使用:状态为:STARTED --> DISPATCHING --> DISPATCHED
这里不会调用Complete。
执行controller后在这里处理返回值:
这里会根据返回值类型来选择对应的处理器:
选择适合的处理器
返回值为callable时:
startCallableProcessing:开启异步上下文,同时将任务扔给线程池
上图中间执行startAsyncProcessing方法, 最终会执行startAsync方法开启异步:
需要注意这里还有一个addListener(): 会向tomcat 注册监听器, 当异步对象超时,异常,tomcat 会进行回调。超时默认值也在这里处理的。
在callable任务执行完毕后,会执行setConcurrentResultAndDispatch方法会将值set到WebAsyncManager# concurrentResult 中, tomcat 下一次事件发生了 可以直接获取该值。
同时会执行asyncContext.dispatch(), 使AsyncContext状态变为DISPATCHING,同时发布OPEN_READ事件任务给tomcat线程,让其下一次获取执行结果返回。
action
上面提到的发布OPEN_READ事件任务,tomcat 线程在AbstractProcessorLight#process 的while 中调用dispatch() 最终就会执行到这里,进而走向Servlet。 对于前面提到的使用Servlet原生异步来开启,再次执行Servlet并不是这里的入口。
上面提到使用callable作为返回值的时候,会调用两次Servlet。 因此Filter 也就会执行多次,因此Spring 中出现了OncePerRequestFilter 类,可以用来控制同一个请求是否需要多次执行filter逻辑
tomcat常用配置参数
server:
tomcat:
# 能接受的最大连接数, 超过则使用backlog,
# 也就是最多处理 max-connections + accept-count 个
max-connections: 10 # accept所能接受的最大数量(默认 8192)
accept-count: 2 # OS backlog, 系统队列。(默认100)
# IO 线程池配置
threads:
max: 2 # 最大工作线程(默认200)
min-spare: 1 # 最小工作线程(默认10)
connection-timeout: 3s # socket 超时,即三次握手后,到发送数据的等待时间(如何模拟)。tomcat初始化会设置默认60s.
keep-alive-timeout: 50s # 有keep-alive(貌似不是HTTP的keep-alive), 则使用该参数作为 连接超时. 同时会返回给客户端
spring:
mvc:
async:
request-timeout: 20s # 返回callable、DeferredResult 时作为超时时间
tomcat相关线程
Acceptor:接受accept连接请求
Poller: 监听连接对象是否发生事件 (Selector)
IO线程池:
处理IO 事件线程池:
默认,min:10, max:200
Catalina线程池:
内部监听等待超时的任务:比如Servlet异步的超时检查。
AbstractProtocol#waitingProcessors
网络相关错误
在tomcat 中,如果在执行Controller 发生IO 异常,大多都会被tomcat内部捕获,且默认只输出debug日志。SpringMVC 将不会捕获到该异常(即全局异常处理器无法捕获)。
EOFException
java.io.EOFException
当对端连接关闭后,这里再次解析时,内部会抛出EOFException。 正常情况下,这里的while 只有在无法继续解析HTTP对象的时候才会退出。
输出debug日志:
在调用setErrorState时,还会再次输出相同的debug 日志
IOException
windows:
java.io.IOException: 远程主机强迫关闭了一个现有的连接。
Linux:
java.io.IOException: Connection reset by peer
java.io.IOException: Broken pipe, 貌似多次触发才会报错
当从socket中读取或写入 数据的时候,对端已经关闭了连接。
从堆栈中可以看到调用的底层网络api:read0/write0
日志内容如下:
win
或:
linux: 多次触发才行。1、2次貌似不行
curl --max-time 1 http://192.168.241.129:8080/test_timeout?c=3
tomcat 捕获错误:
通常情况下在下面两种场景中会抛出IOException
- controller 中 使用 Servlet 对象主动向底层写手动调用flush时,连接被关闭:
HandleIOException方法:内部调用 setErrorState, 如果开启了DEBUG ,则输出上面日志信息
- 当Servlet处理完成,向客户端响应数据时连接被关闭:
Filter 捕获异常
由于在发生异常时,tomcat会执行下面方法,进行保存Exception信息。如果是在filter前发生的异常,可以通过获取response的错误信息(比如主动调用flush 时,连接关闭), 连接关闭貌似不能马上通知到应用层,
当response.getWrite() 调用flush 时, 会首先进入该方法,执行ob.flush(), 当内部执行抛出IOException时,这里会设置error = true。 当下一次执行write 、 flush 时会判断该状态,如果已经是true,那么直接return。不会调用底层的IO接口。
handleIOException: 这里会设置异常到Request对象中。
filter 中通过对象获取异常:
Exception attribute = (Exception) request.getAttribute(RequestDispatcher.ERROR_EXCEPTION);
Linux 中的网络异常分析
Connection reset by peer:
当服务端发起RST 后,客户端继续从缓存区读取数据将会抛出该异常。 服务端正常关闭通常是发送FIN 优雅关闭,因此不会触发该异常。
server:
import socket
import struct
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server.bind(("0.0.0.0", 12345))
server.listen(1)
print("Server is listening...")
conn, addr = server.accept()
print(f"Connection from {addr}")
# 配置 SO_LINGER,强制发送 RST 包
linger_struct = struct.pack('ii', 1, 0) # 开启 SO_LINGER,超时时间为 0
conn.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, linger_struct)
print("Closing connection with RST...")
conn.close() # 强制关闭连接,发送 RST 包
server.close()
**client:
**
import socket
import time
try:
client = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
client.connect(("127.0.0.1", 12345))
time.sleep(1) # 确保收到了RST,后面的操作都会触发异常
client.send(b"Hello, server!") # 尝试发送消息。
# 强制探测连接状态
data = client.recv(1024) # 这里会触发异常,因为连接被重置
print(f"Received: {data}")
except socket.error as e:
print(f"Socket error: {e}")
这里是构造的一个 RST、ACK包,表示收到了一个非法的包(连接已经关闭)
Broken pipe:
当client正常关闭后,client 继续像socket缓冲区写入数据,将会收到SIGPIPE信号,应用将会停止。同时不会产生core dump
第一次write 不会收到SIGPIPE 信号。当调用第二次时就会触发该信号,同时抛出异常,结束进程。
当client 关闭连接后,会发送FIN数据包给server,server会立即回复ACK。当调用write将数据发送给对方时,对方已经关闭了socket,处于不接受数据的状态,此时收到数据后立即回复RST,使client立即关闭。
由于server 本身也已经关闭socket,因此第二次调用write 就会收到sigpipe信号。
需要注意,一些框架像glibc可能会忽略SIGPIPE信号,防止进程终止。 因此为了模拟需要开启默认行为:
// 恢复 SIGPIPE 的默认行为,
signal(SIGPIPE, SIG_DFL);
struct sigaction sa;
sigaction(SIGPIPE, NULL, &sa); // 获取 SIGPIPE 当前行为
if (sa.sa_handler == SIG_IGN) {
printf("SIGPIPE is ignored\n");
} else if (sa.sa_handler == SIG_DFL) {
printf("SIGPIPE has default behavior\n");
} else {
printf("SIGPIPE has a custom handler\n");
}