bigywater.tech
爬虫工程 · 13

TLS 会话复用:握手耗时占一次请求的多少

每次新建 HTTPS 连接都要做 TLS 握手。握手到底占一次请求多少时间、会话复用能省多少、你用的库会不会复用,这篇用本机的 TLS 服务器逐项量。顺带发现:一个会话票据在什么情况下会变得不可复用。

73%
完整握手占一次小 HTTPS 请求总耗时的比例(P-256 证书、TLS 1.3、本机回环)
3.993 → 0.525 ms
RSA-4096 证书的握手耗时:完整握手对会话复用
19 / 20
curl_cffi Session 每个请求新建连接时复用了 TLS 会话的连接数(requests、aiohttp 都是 0)

会话票据什么时候失效

实测 · 本机执行输出
  服务器直接关 TCP(不发 close_notify);客户端收到响应就立刻取会话 → 第 2 次连接复用了会话:True
  服务器直接关 TCP(不发 close_notify);客户端一直读到 EOF 再取会话 → 第 2 次连接复用了会话:False
  服务器先发 close_notify 再关;客户端收到响应就立刻取会话 → 第 2 次连接复用了会话:True
  服务器先发 close_notify 再关;客户端一直读到 EOF 再取会话 → 第 2 次连接复用了会话:True

客户端拿到的会话票据,在“连接被不干净地关掉之后又读到 EOF”时会变得不可复用:服务器直接关 TCP(没有 TLS 的 close_notify)、客户端读到 EOF 再取会话,第 2 次连接就复用失败;服务器先发 close_notify,或客户端在收到响应时就取走会话,则正常。这是实测现象,原因是我的解释,没有读 OpenSSL 的实现。

握手占多少

实测 · 本机执行输出
  完整握手:TCP 连接 0.089 ms + TLS 握手 0.628 ms + 请求/响应 0.145 ms = 0.862 ms;握手占 73%
  会话复用:TCP 连接 0.089 ms + TLS 握手 0.528 ms + 请求/响应 0.132 ms = 0.750 ms;握手占 70%
  复用那组里,客户端确认会话被复用的比例 100%
  握手耗时:复用 / 完整 = 0.84

用 P-256 证书、TLS 1.3,在本机回环上,完整握手 0.628 ms,TCP 连接 0.089 ms,请求响应 0.145 ms,握手占 73%。会话复用把握手降到 0.528 ms(0.84 倍)。TLS 1.3 的复用仍然要做一次密钥交换,所以省得不多;没有网络往返的本机环境也会低估真实世界里握手的比例。

证书类型的影响

实测 · 本机执行输出
  证书              完整握手      会话复用
  ec256        0.625 ms   0.522 ms
  ec384        1.012 ms   0.533 ms
  rsa2048      1.094 ms   0.521 ms
  rsa4096      3.993 ms   0.525 ms

服务器每次完整握手都要签名,密钥类型差别很大:P-256 0.625 ms、P-384 1.012 ms、RSA-2048 1.094 ms、RSA-4096 3.993 ms。会话复用时不再需要证书签名,不管什么证书都只要约 0.52 ms。所以服务器证书越“重”,客户端复用会话的收益越大。

TLS 1.2 与 1.3

实测 · 本机执行输出
  TLS 1.3:完整握手 0.591 ms,会话复用 0.495 ms(复用成功比例 100%)
  限制到 TLS 1.2:完整握手 0.450 ms,会话复用 0.155 ms(复用成功比例 100%)

本机回环没有往返延迟,看到的只是计算量:TLS 1.2 完整握手 0.450 ms、复用 0.155 ms;TLS 1.3 完整握手 0.591 ms、复用 0.495 ms。往返次数的差别(TLS 1.3 少一个往返)要在有延迟的网络上才能体现,我没有测。

各库会不会复用

实测 · 本机执行输出
  requests.get ×20:服务器收到 20 个请求,其中复用了 TLS 会话的 0 个,连接数 20
  同一个 requests.Session,每次 Connection: close ×20:服务器收到 20 个请求,其中复用了 TLS 会话的 0 个,连接数 20
  curl_cffi Session,每次 Connection: close ×20:服务器收到 20 个请求,其中复用了 TLS 会话的 19 个,连接数 20
  curl_cffi.requests.get ×20(每次新建 Session):服务器收到 20 个请求,其中复用了 TLS 会话的 0 个,连接数 20
  aiohttp,force_close=True ×20:服务器收到 20 个请求,其中复用了 TLS 会话的 0 个,连接数 20

Python 的 ssl 不会自动复用会话,要手动把上一次的会话对象传给 wrap_socket(session=…)。在 urllib3 与 aiohttp 的连接代码里我没搜到传递会话的地方,实测它们 20 个新连接 0 个复用。curl_cffi(libcurl)在同一个 Session 内缓存会话票据,19/20 复用;每次新建 Session 就没有缓存了。

分步演示

TLS 会话复用
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。握手各段用 time.perf_counter 在客户端分段计时(socket.create_connection、SSLContext.wrap_socket 的握手、发送与接收);会话复用用 wrap_socket(session=…) 并检查 session_reused;完整握手与复用交替各做 200–300 次取中位数;证书用 openssl 生成(EC P-256、P-384,RSA-2048、RSA-4096,自签名,客户端不校验);服务器和客户端都是 Python 的 ssl 模块(链接的是 OpenSSL 3.6.5),服务端记录每条连接的 session_reused;证书由命令行 openssl 4.0.3 生成
    • 没有验证:真实网络里握手的往返次数对总耗时的影响(本机回环没有延迟);TLS 1.3 的 0-RTT;证书链校验的耗时(客户端关闭了校验);nginx 等真实服务器的会话缓存与票据密钥轮换;票据的有效期;为什么不干净的关闭会让会话失效的底层原因;让 requests 或 aiohttp 手动复用会话的做法
    京ICP备2026067502号-1京公网安备11010502063487号