服务器会在连接空闲一段时间后关掉它。爬虫的 Session 里躺着一条早就被对端关了的连接,下一个请求会怎样?这篇用本机服务器把几种情形都复现一遍:悄悄重建、真正报错的竞态,以及 Keep-Alive 提示头到底有没有人听。
间隔 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 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 重试在后面单独讲)。
第 1 个响应头:Keep-Alive: timeout=1 客户端等了 2.0 秒(超过 timeout=1,但服务器实际 3.0 秒才关)再请求:服务器累计 1 条连接 → 客户端没有因为这个提示放弃旧连接
服务器可以在响应里写 Keep-Alive: timeout=1 告诉客户端空闲多久会被关。实测 requests 不会据此提前放弃连接:等了 2.0 秒、超过提示,只要服务器还没真关,旧连接就继续被复用。在 urllib3 和 requests 的顶层模块里我只搜到默认请求头里的 keep-alive,没找到读取这个提示的代码。