浏览器一次请求到 uvicorn/FastAPI 的底层流程(含 epoll/FD)
- 浏览器发起 HTTP 请求 → 建立 TCP 连接
浏览器向服务器的:
IP:PORT(例如 1.2.3.4:8080)
发起三次握手,建立 TCP 连接。
- uvicorn 在服务器上监听端口
uvicorn(Python 级别)做:
sock = socket()
sock.bind(('0.0.0.0', 8080))
sock.listen()
这一步创建了:
监听 socket(FD),比如 fd=7
随后 uvicorn 会将这个 socket 加入事件循环:
loop.add_reader(fd=7, callback=on_accept)
如果使用 uvloop,该操作最终会变成:
libuv 调用 epoll_ctl(ADD fd=7)
- uvloop(libuv)使用 epoll 监听这些 FD
libuv 在 Linux 下:
为事件循环创建一个 epoll 实例
调用 epoll_ctl() 注册 fd=7
进入循环,不停执行:
epoll_wait()
这是关键点:
🔥 内核不会主动"调用回调"
而是:
✔ 内核只是在 epoll 实例里标记:
"某个 FD 已经准备好读/写了"
然后等用户空间 epoll_wait() 返回。
- 浏览器发来第一个 HTTP 报文 → FD 可读(EPOLLIN)
当客户端发送 HTTP 请求报文,到达服务器的 fd=9(已 accept 的连接):
内核将 fd 标记为"可读"
放入 epoll 的 ready-list
然后 epoll_wait() 被唤醒并返回
没有任何"内核调用回调"的行为
只有 epoll_wait 主动返回就绪 FD 列表
- uvloop 的事件循环得到事件 → 调用对应回调
uvloop 的事件循环不断运行:
epoll_wait() → ready FDs → 调用 Python 层对应回调
例如:
fd=7(监听端口)有 EPOLLIN → on_accept 被调用
fd=9(某个客户端连接)有 EPOLLIN → on_read 被调用
这些回调函数最终进入 uvicorn 的 HTTP 解析逻辑。
- HTTP/1.1 Keep-Alive → 同一个 FD 多次进入 ready list
浏览器下一次发请求(仍在同一 TCP 连接上):
内核再次检测到 fd=9 可读
再次将 fd=9 放入 epoll ready-list
uvloop 再次从 epoll_wait 得到它
uvicorn 再次处理(这一次就是第二个 HTTP 请求)
FD 不会关闭
FD 会反复进入 ready-list
直到浏览器或服务器主动关闭连接
⭐ 最终整理后的版本(推荐你保存)
下面是一版简洁 + 完全准确的表述:
✅ 从浏览器发起 HTTP 请求到 uvicorn/FastAPI 的底层机制(含 FD / epoll / uvloop)
浏览器发起 HTTP 请求 → 建立 TCP 连接
浏览器与服务器的监听端口(例如 8080)建立 TCP 连接。
uvicorn 监听端口,创建监听 FD
uvicorn 创建 socket FD(如 fd=7)并调用事件循环注册监听。
uvloop(基于 libuv)将 FD 注册到 epoll
libuv 调用 epoll_ctl(EPOLL_CTL_ADD),将监听 FD 加入 epoll。
事件循环执行 epoll_wait() 等待就绪 FD
内核不会主动调用回调,只会在 epoll 实例中标记就绪 FD。
浏览器发送 HTTP 报文 → FD 可读
内核将该 FD 标记为 EPOLLIN,epoll_wait 被唤醒并返回该 FD。
uvloop 拿到 ready FD → 调用对应的回调函数
对监听 FD → accept 回调
对连接 FD → read 回调(进入 uvicorn HTTP parser)
FastAPI 处理请求并返回响应
Keep-Alive 下:同一个 FD 持续复用
当浏览器再次发起请求,FD 再次可读,被 epoll 标记,再次被 uvloop 回调处理。
连接关闭时,FD 被 epoll 删除并关闭
当浏览器或服务器关闭连接,FD 才真正关闭和消失。
🎯 最终一句话
一次 HTTP 请求不会导致 FD 消失,只要 Keep-Alive 存在,同一个 FD 会多次进入 epoll 的 ready list;只有 TCP 连接断开时 FD 才会被关闭。