每次新建 HTTPS 连接都要做 TLS 握手。握手到底占一次请求多少时间、会话复用能省多少、你用的库会不会复用,这篇用本机的 TLS 服务器逐项量。顺带发现:一个会话票据在什么情况下会变得不可复用。
服务器直接关 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.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 就没有缓存了。