bigywater.tech
网络协议 · 93

会话恢复与 0-RTT:省掉握手的代价

每次 HTTPS 连接都做完整握手太浪费:TLS 的会话恢复让客户端带着上次的“票”回来。这篇测它省了什么(1.2 里是一个往返,1.3 里是证书和计算,不是往返)、ClientHello 里多了什么、ticket 能用几次,以及传说中的 0-RTT 在真实服务器上到底开不开。

6563 → 2800 字节
TLS 1.3 完整握手与恢复握手里服务器→客户端的字节数(中位)
322 → 155 ms
TLS 1.2 完整握手与恢复握手的耗时中位数,恰好省一个往返
Max Early Data: 0
example.com 的 ticket 里允许的早期数据量:0-RTT 被关闭

1.3 恢复省什么

实测 · 本机执行输出
  完整握手:耗时(ms)[807, 155, 802, 166, 175],中位 175;服务器→客户端 6563 字节,客户端→服务器 1689 字节(中位,包含收发各一次 HTTP 请求和响应)
  恢复握手:耗时(ms)[169, 1503, 169, 154, 600],中位 169;session_reused = [True, True, True, True, True];服务器→客户端 2800 字节,客户端→服务器 1960 字节(中位)
  session 对象:SSLSession,has_ticket = True,ticket 生存期提示 7200 秒(2.0 小时)

省证书和计算,不省往返:耗时 175 对 169 ms,但服务器→客户端字节降到不到一半。

线上差别

实测 · 本机执行输出
  完整握手的 ClientHello 1526 字节,扩展:['renegotiation_info', 'server_name', 'ec_point_formats', 'supported_groups', 'session_ticket', 'ALPN', 'encrypt_then_mac', 'extended_master_secret', 'signature_algorithms', 'supported_versions', 'psk_key_exchange_modes', 'key_share']
  恢复握手的 ClientHello 1797 字节,扩展:['renegotiation_info', 'server_name', 'ec_point_formats', 'supported_groups', 'session_ticket', 'ALPN', 'encrypt_then_mac', 'extended_master_secret', 'signature_algorithms', 'supported_versions', 'psk_key_exchange_modes', 'key_share', 'pre_shared_key']
  恢复的 ClientHello 多了 pre_shared_key 扩展(267 字节:里面是 ticket 本身 + 一个 binder,用来证明客户端知道密钥),并且它必须是最后一个扩展
  完整握手的 ServerHello 扩展:['key_share', 'supported_versions'];恢复握手的 ServerHello 扩展:['pre_shared_key', 'key_share', 'supported_versions'](pre_shared_key 表示服务器接受了恢复)
  服务器在 ServerHello 之后发的加密记录:完整握手 [3859, 477, 940, 22, 19],恢复握手 [90, 477, 940, 22, 19]——恢复时不再发证书(完整握手的那个 3000+ 字节记录),只剩 Finished 之类的小记录

恢复的 ClientHello 多一个 267 字节的 pre_shared_key,ServerHello 多 pre_shared_key,不再发证书。

1.2 恢复

实测 · 本机执行输出
  TLS 1.2 完整握手:[327, 891, 322, 306, 319],中位 322 ms;恢复握手:[157, 155, 154, 163, 153],中位 155 ms;session_reused = [True, True, True, True, True]
  对比 TLS 1.3:完整 175 ms,恢复 169 ms——TLS 1.3 的完整握手本来就只要 1 个往返,恢复省的是证书和计算,不省往返

完整 2 个往返,恢复 1 个:322 → 155 ms。

ticket 重复使用

实测 · 本机执行输出
  连续 4 次用同一个 ticket:(是否恢复, 握手 ms) = [(True, 166), (True, 190), (True, 162), (True, 162)]
  TLS 1.3 规范建议 ticket 只用一次(防重放和跨连接关联),服务器可以拒绝重复使用;这里看到服务器是否接受重复使用

同一个 ticket 连用 4 次都成功,但会让不同连接可被关联。

0-RTT

实测 · 本机执行输出
  第一次连接(openssl):Protocol  : TLSv1.3; TLS session ticket lifetime hint: 64800 (seconds); Max Early Data: 0; Protocol  : TLSv1.3; TLS session ticket lifetime hint: 64800 (seconds); Max Early Data: 0
  第二次(带 ticket 并要求发 early data):Reused, TLSv1.3, Cipher is TLS_AES_256_GCM_SHA384; Early data was not sent
  服务器收到了 1 个 NewSessionTicket(openssl 在输出里打印了每个 ticket 的信息);Max Early Data 为 0 表示服务器没有允许 0-RTT——这个站点关闭了 0-RTT

example.com 不允许早期数据,恢复成功但 early data 没发出。

分步演示

会话恢复与 0-RTT
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。对 example.com:443 的真实 TLS 连接(Python ssl + 抓包 en0,以及 Homebrew 的 openssl 4.0.3 s_client);耗时样本里有被网络抖动拉到 600~1500 ms 的离群值,所以只看中位数;Python 记录的 ticket 生存期(7200 秒)与服务器通告的(64800 秒)不同,我没有查原因
    • 没有验证:0-RTT 的收益和重放风险(服务器关闭了它);ticket 加密密钥的轮换;跨服务器(负载均衡后)的 ticket 共享;QUIC 的 0-RTT;TLS 1.3 的 PSK-only(不带 key_share)恢复模式;客户端不重用 ticket 的实现
    京ICP备2026067502号-1京公网安备11010502063487号