捂脸斋

江山风月 🌘 本无常主 🌈 闲者便是主人 🍃🍃🍃

tomcat nio 请求链路

A: telnet 发起连接

x

B: Acceptor 线程接受连接

org.apache.tomcat.util.net.Acceptor

实现了 Runnable 接口, run() 方法内是一个循环。

1
2
3
4
5
6
7
8
9
10
11
void run() {

// 1 门栓控制最大连接数
endpoint.countUpOrAwaitConnection();

// 2 阻塞直到连接产生
socket = endpoint.serverSocketAccept();

// 3 把 socket channel 丢给 Poller 中的队列
endpoint.setSocketOptions(socket);
}
  1. 若已达到最大连接数,阻塞;否则通过。

    LimitLatch门闩控制最大连接数。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    protected void countUpOrAwaitConnection() throws InterruptedException {
    if (maxConnections==-1) {
    return;
    }
    LimitLatch latch = connectionLimitLatch;
    if (latch!=null) {
    latch.countUpOrAwait();
    }
    }
  2. 执行endpoint.serverSocketAccept()并阻塞;若有连接事件,产生socket

    1
    2
    3
    4
    5
    6
    7
    protected SocketChannel serverSocketAccept() throws Exception {
    // 这里是阻塞点, 直到有连接产生
    SocketChannel result = serverSock.accept();
    // Bug does not affect Windows platform and Unix Domain Socket. Skip the check.
    // ...
    return result;
    }

    a. 执行serverSock.accept(),阻塞直到有连接产生。

    操作文件描述符,完成 tcp 握手,会调用系统层方法sun.nio.ch.ServerSocketChannelImpl#accept()

    b. 其他。

  3. 执行setSocketOptions(),将socket注册到Poller的队列中。
    org.apache.tomcat.util.net.NioEndpoint#setSocketOptions()`

    1
    2
    3
    4
    5
    6
    7
    protected boolean setSocketOptions(SocketChannel socket) {
    // Allocate channel and wrapper
    NioSocketWrapper socketWrapper = null;
    NioChannel channel = null;
    NioSocketWrapper newWrapper = new NioSocketWrapper(channel, this);
    poller.register(socketWrapper);
    }

    a. 初始化 NioSocketWrapperNioChannel
    b. 将 socket 包装成 PollerEvent 注册给 Poller只关心读事件。

  4. 回到循环头。

C: Poller 线程监听读事件

org.apache.tomcat.util.net``.NioEndpoint.Poller

实现了 Runnable 接口,循环执行 run() 方法,本质是 Selector

之前的PollerEvent都存放在events中,是一个同步队列。SynchronizedQueue

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public void run() {
while (true) {
// 1
hasEvents = events();
if (wakeupCounter.getAndSet(-1) > 0) {
// If we are here, means we have other stuff to do
// Do a non blocking select
keyCount = selector.selectNow();
} else {
keyCount = selector.select(selectorTimeout);
}
wakeupCounter.set(0);

// 2
Iterator<SelectionKey> iterator = keyCount > 0 ? selector.selectedKeys().iterator() : null;
while (iterator != null && iterator.hasNext()) {
SelectionKey sk = iterator.next();
iterator.remove();
NioSocketWrapper socketWrapper = (NioSocketWrapper) sk.attachment();
// Attachment may be null if another thread has called
// cancelledKey()
if (socketWrapper != null) {
processKey(sk, socketWrapper);
}
}
}
}
  1. 处理队列中的所有PollerEvent

    把刚建立的连接,真正注册到 Selector,开始监听读事件。

  2. selector进行一次select,如果有读事件发生,会被 select 到。

  3. 如果 select 到读事件,遍历所有的读事件,依次调用 processKey(sk, socketWrapper)

    1
    2
    3
    4
    protected void processKey(SelectionKey sk, NioSocketWrapper socketWrapper) {
    // ...
    processSocket(socketWrapper, SocketEvent.OPEN_READ, true);
    }
    1
    2
    3
    4
    5
    6
    public boolean processSocket(SocketWrapperBase<S> socketWrapper,
    SocketEvent event, boolean dispatch) {
    sc = createSocketProcessor(socketWrapper, event);
    Executor executor = getExecutor();
    executor.execute(sc);
    }

    获取Executor,提交给工作线程。

  4. 循环回到 1。

D: 进入工作线程

实现了 Runnable 接口,循环执行 run() 方法。

进入工作线程后,执行org.apache.tomcat.util.net``.NioEndpoint.SocketProcessor#doRun()

这里有大量代码,最后会执行到org.apache.coyote.http11.Http11Processor#service()

  • service()的主要逻辑

    1
    2
    这里只读取请求头(通过换行符判断),进行请求格式校验;不读请求体。
    请求体数据流:从网卡进入到内核 socket 接收缓冲区(RCV BUF);还没有进入用户进程缓冲区(Tomcat)。
    1
    2
    3
    4
    5
    6
    7
    8
    何时读请求体?
    Controller @RequestBody

    Spring 调用 inputStream.read()

    Tomcat 从内核缓冲区读取 Body 字节

    Jackson 转成 Java 对象
    • 初始化处理上下文

      设置 socketWrapper、重置 keepAlive/openSocket/readComplete,并记录请求处理阶段。

    • 进入主循环处理一个或多个请求(keep-alive

      循环条件包括:无错误、允许长连接、非异步、未升级、未暂停、sendfile 已完成。

    • 解析请求行与请求头

      处理不完整读取、HTTP 协议版本识别(HTTP/1.11.00.9)、暂停状态、读取超时设置等。

      1
      2
      socketWrapper.read(buffer)
      读取到连续两个换行符\r\n\r\n结束。换行符前面是请求头,后面是请求体。
    • 处理 Upgrade 升级请求

      校验升级协议,克隆并补齐请求体,发送 101,切换到升级协议并返回 UPGRADING

    • 控制 keep-alive 次数

      执行 getAdapter().service(request, response),并处理各类异常与错误状态。

    • 请求收尾与复用

      结束输入输出、更新统计、在可复用时 nextRequest(),恢复读超时,处理 sendfile

    • 根据最终状态返回 SocketState

      按错误、异步、升级、sendfileopenSocket/readComplete 等条件返回 CLOSEDLONGOPENSENDFILEUPGRADING

    总结

    image-20260417165431499

telnet 发送请求

HTTP1.1 tcp 复用

可以在一个 TCP 连接中连续发送请求。
请求 1 没有响应之前,tomcat 不处理请求 2。请求 1 响应之后,tomcat 才处理请求 2。
HTTP 1.1 同一个连接,是串行、排队、顺序执行!
这两个请求会在同一个线程执行。

HTTP 1.1 同一个连接,是串行、排队、顺序执行!

这两个请求会在同一个线程执行。

image

image (1)

HTTP1.0 不能 tcp 复用

只能发一个请求,服务端会主动断开 TCP。

image (2)

HTTP1.1 分批发送 Header 和 Body

tomcat 完成请求头的解析后,就会派发给 servlet;不必等 body 发送完成。

在 servlet 中,读请求体会阻塞,直到客户端发完。

  1. 先发送请求头

    image

  2. tomcat 收到可读事件,工作线程解析请求头。

    image (1)

  3. tomcat 进入Controller,read 阻塞。

    image (2)

  4. 客户端发送请求体

    image (3)

  5. 服务端 read 成功。

    image (4)