bigywater.tech
爬虫工程 · 4

keep-alive 空闲超时:服务端先关掉连接,客户端怎么发现

服务器会在连接空闲一段时间后关掉它。爬虫的 Session 里躺着一条早就被对端关了的连接,下一个请求会怎样?这篇用本机服务器把几种情形都复现一遍:悄悄重建、真正报错的竞态,以及 Keep-Alive 提示头到底有没有人听。

CLOSE_WAIT
服务器空闲超时关闭后,客户端套接字的状态;下一个请求自动换新连接
0 次
服务器空闲 0.5 秒就关、客户端每 0.7 秒请求一次:6 次请求的异常次数
3 个
用 Retry 重试后服务器收到的请求数(被断开的那次也收到了)

空闲超时之后:悄悄重建

实测 · 本机执行输出
  间隔 0.2 秒:第 2 个请求 200,服务器累计 1 条连接;客户端那端的套接字状态 发请求前 ['ESTABLISHED'] → 等待之后 ['ESTABLISHED'];第 2 个请求耗时 1.31 ms
  间隔 1.5 秒:第 2 个请求 200,服务器累计 2 条连接;客户端那端的套接字状态 发请求前 ['ESTABLISHED'] → 等待之后 ['CLOSE_WAIT'];第 2 个请求耗时 2.12 ms

服务器在连接空闲 1.0 秒后关闭它。隔 0.2 秒再请求,复用同一条连接;隔 1.5 秒,lsof 看到客户端那端已经是 CLOSE_WAIT(对端的 FIN 到了,本端还没关),请求依然成功,服务器那边是第 2 条连接。

urllib3 怎么发现连接断了

实测 · 本机执行输出
  urllib3 2.7.0 connectionpool.py 第 288 行:if conn and is_connection_dropped(conn):
  HTTPConnection.is_connected 的实现(urllib3 源码):
     351 |     @property
     352 |     def is_connected(self) -> bool:
     353 |         if self.sock is None:
     354 |             return False
     355 |         return not wait_for_read(self.sock, timeout=0.0)
  服务器空闲 0.5 秒就关,客户端每隔 0.7 秒请求一次,共 6 次请求:异常 0 次,服务器累计 6 条连接

从池里取连接时,urllib3 调 is_connection_dropped(2.7.0 的 connectionpool.py 第 288 行),它读 is_connected:套接字存在,并且 wait_for_read(sock, timeout=0.0) 不可读,才算还连着——对端关闭会留下可读的 EOF,所以被判为断开。代价是每次都得重新建连接:空闲超时比请求间隔短时,Session 的复用等于没有。

竞态:请求已经发出才断开

实测 · 本机执行输出
  默认(max_retries=0):第 2 个请求(在复用的连接上被服务器直接断开)→ ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response'));服务器累计 1 条连接,收到 2 个请求
  Retry(total=1):第 2 个请求(在复用的连接上被服务器直接断开)→ 成功 200;服务器累计 2 条连接,收到 3 个请求

检查只发生在取连接那一刻。如果服务器恰好在收到请求之后关了连接,客户端抛出 ConnectionError(内含 RemoteDisconnected)。给 Session 挂 Retry(total=1) 可以让 GET 重试成功,但注意服务器已经收到了第一次请求——对有副作用的请求,重试可能让它被处理两次(POST 重试在后面单独讲)。

Keep-Alive: timeout 提示

实测 · 本机执行输出
  第 1 个响应头:Keep-Alive: timeout=1
  客户端等了 2.0 秒(超过 timeout=1,但服务器实际 3.0 秒才关)再请求:服务器累计 1 条连接 → 客户端没有因为这个提示放弃旧连接

服务器可以在响应里写 Keep-Alive: timeout=1 告诉客户端空闲多久会被关。实测 requests 不会据此提前放弃连接:等了 2.0 秒、超过提示,只要服务器还没真关,旧连接就继续被复用。在 urllib3 和 requests 的顶层模块里我只搜到默认请求头里的 keep-alive,没找到读取这个提示的代码。

分步演示

keep-alive 空闲超时
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / Python 3.14.8 / requests 2.34.2 / aiohttp 3.14.3 / lxml 6.1.1(系统 Python);所有请求只发给本机自己起的测试服务器(127.0.0.1,端口 9960 以上,不访问任何真实网站);服务器可以按实验需要制造延迟、断连、压缩、重定向等行为,并记录收到的原始请求;计时用 time.perf_counter,数字每次运行会有小幅抖动。服务器用 Python 标准库 http.server 的连接超时(Handler 的 timeout 属性)实现“空闲 N 秒后关闭”;客户端套接字状态用 lsof -nP -iTCP:端口 读取(这台 macOS 上 netstat 看不到回环连接);竞态用服务器在收到第 2 个请求后直接 shutdown 并关闭套接字来模拟;源码位置取自 urllib3 2.7.0
    • 没有验证:真实服务器(nginx、负载均衡)的 keep-alive 超时与 Connection: close 的发送时机;客户端恰好在服务器关闭的同一瞬间发请求这类真正的时间竞态(这里是用服务器主动断开来确定性地模拟);aiohttp 等其它客户端对已关闭连接的处理;HTTP/2 连接的空闲处理
    京ICP备2026067502号-1京公网安备11010502063487号