
去年我排查一个接口偶发超时抓包后发现请求体还没发出TLS握手已经消耗了将近三个RTT。当时我就意识到很多人天天和 HTTPS 打交道却未必能说清握手流程里每一次网络往返到底在做什么。这篇文章想用大白话把TLS握手流程完整拆一遍重点讲清楚一个核心指标RTTRound-Trip Time网络往返时间。从 TLS 1.2 的 2-RTT 讲到 TLS 1.3 的 0-RTT弄清楚版本演进到底省了什么、代价是什么。如果你是后端研发、运维或者只是好奇为什么 HTTPS 总是比 HTTP 慢那么一点这篇都值得看。读完你至少能看懂抓包里的 ClientHello、ServerHello也能明白为什么 TLS 1.3 敢砍掉这么多加密套件。1. 为什么TLS握手要专门关心“几次往返”1.1 一个简单的请求网络层要跑几个来回先把“RTT”这个词说清楚。RTT 就是客户端发出去一个包服务器收到后回应一个包这一个来回所消耗的时间。普通局域网内 RTT 可能不到 1 毫秒但公网环境下几十毫秒是常态跨地域网络到一百多毫秒也不稀奇。你打开一个 HTTP 网页理想情况下只要 1 个 RTT浏览器发出 GET 请求服务器返回 HTML。但如果你访问的是 HTTPS 网站在 HTTP 请求之前还要先做 TCP 三次握手再走 TLS 握手。这里有个很多人忽略的细节TCP 三次握手本身也是网络往返通常占 1 个 RTT。这个 RTT 跟 TLS 握手是两码事。很多文章说 TLS 1.2 是 2-RTTTLS 1.3 是 1-RTT说的是 TLS 握手自身的开销没有算 TCP 建立连接的那一次。把两者加起来一个全新 HTTPS 请求的实际网络链路是TCP 握手 1 个 RTTTLS 握手 2 个或 1 个 RTT然后才是 HTTP 请求 1 个 RTT。所以 TLS 1.2 完整建连总共需要 4 个 RTTTLS 1.3 则是 3 个 RTT。如果网络延迟是 100 毫秒光建连就差了 100 毫秒这个差距在弱网环境下非常明显。1.2 一次握手真正要完成的“三件大事”为什么 HTTPS 要这么麻烦非要在业务数据前先握手因为 TLS 握手的目的是建立一条安全通道而安全通道必须同时满足三件事。第一件是身份验证。你访问某个网站时要确认对面真的是那个网站而不是中间某个冒牌货。这个靠服务器证书来完成证书由受信任的证书机构签发。如果跳过身份验证攻击者可以伪装成服务器把你的数据骗走。第二件是密钥协商。双方要共同计算出一套对称加密密钥用这套密钥来加密后续所有业务数据。对称加密快适合大规模数据传输但难点在于双方如何安全地拿到同一把密钥这就是握手阶段要解决的核心问题。第三件是完整性保护。即使数据被加密攻击者也可能截获后篡改所以还需要让接收方能够识别数据是否被动过手脚。这一般通过消息认证码来实现密钥同样来自握手阶段。把三件事合在一起你会发现握手不是可有可无的仪式而是安全通信的根基。砍掉任何一件事HTTPS 就和明文 HTTP 没什么本质区别了。1.3 握手不是加密通信而是建立加密通信的“钥匙办理窗口”很多人会把 TLS 握手理解成“握手过程本身就是加密的”这其实不完全准确。准确地说TLS 1.2 握手阶段里除了最后一步切换加密套件后的 Finished 消息之外大部分消息都是明文或者只做简单签名的。比如 ClientHello 和 ServerHello任何抓包工具都能看到内容。这在安全性上是可以接受的因为这些消息里的随机数、套件列表本身不算机密真正的秘密是后续密钥交换材料。我更愿意把握手比作去银行办业务的流程。TCP 三次握手相当于你先到达银行门口确认营业时间。TLS 握手就像在柜台核验身份证、填申请表然后领到一把保险箱钥匙。这个“领钥匙”的过程公开进行没有关系真正要紧的是钥匙本身不能让别人拿到。你是不是发现每次新办一张银行卡其实不算慢慢的是排队、核验身份这些前置流程。TLS 握手也一样它本身不传输业务数据所以它的 RTT 完全是“纯开销”唯一的价值是让后面的数据可以安全地飞。理解了这一点你就会明白业界为什么拼命优化握手的 RTT。2. TLS 1.2 的两次往返来看看每一步到底在干什么2.1 第一次往返ClientHello 和 ServerHelloTLS 1.2 的握手从客户端发出一条 ClientHello 开始。这条消息里包含客户端支持的 TLS 版本列表、加密套件列表、一个随机数 client_random以及各种扩展。你可以把它理解成去餐厅吃饭时服务员递过来的那份菜单上面写着“我们店能做的菜”。服务器收到后会从里面挑一个自己认识且安全的组合然后回复 ServerHello。ServerHello 里包含服务器选定的 TLS 版本、选定的加密套件、一个随机数 server_random以及服务器自己的证书。证书会在后续单独的消息中发送有些实现会把 Certificate 消息紧跟 ServerHello 发出但它们合在一起都属于第一次往返的响应。客户端收到 ServerHello 和证书后第一件事是验证证书链确认这个证书是可信机构签发的且域名匹配、没有过期。这一步看起来简单实际开销不小尤其是证书链较长或者需要在线查询吊销状态时即使是在同一个 RTT 内也会增加处理时延。这里要说一个关键点第一次往返结束时双方已经交换了 client_random 和 server_random但还没有任何能保护数据的对称密钥。如果你抓包此时看到的都是明文。真正把通信保护起来的是第二次往返里发生的事。2.2 第二次往返密钥材料交换与 Finished 确认TLS 1.2 的第二次往返是整个握手的关键步骤。客户端在验证完证书后需要生成一个预备主密钥在 RSA 密钥交换模式下客户端用服务器公钥加密它后发送给服务器在 ECDHE 模式下客户端会生成椭圆曲线密钥对把公钥发过去。无论哪种方式这个密钥交换材料就是握手的“核心机密”。当服务器拿到密钥交换材料后双方各自用 client_random、server_random 和预备主密钥通过伪随机函数计算出主密钥再派生出后续加密通信所需的一系列密钥。这个计算过程是在本地完成的不需要再通信所以握手从这里开始进入加密世界。紧接着客户端发送 ChangeCipherSpec 告诉服务器“我这边接下来开始加密了”并发送一条用新密钥加密的 Finished 消息。服务器同理也发 ChangeCipherSpec 和 Finished。Finished 消息的作用是让双方确认我刚才算出来的密钥和你算出来的密钥是一致的。如果中间有人篡改了握手消息那么双方用被篡改的随机数计算出的密钥就不一致Finished 校验也会失败。所以第二次往返不仅是密钥协商也是对整个握手过程的完整性校验。TLS 1.2 里这完整的前后两次往返加起来就是 2-RTT。第一次往返交换随机数和证书第二次往返交换密钥材料并确认少了任何一次都建立不起来安全通道。2.3 为什么套件协商和证书验证绕不开看到这里你可能会想能不能把第二次往返的内容合并到第一次往返里可以这正是 TLS 1.3 做的事。但在那之前TLS 1.2 里有个根深蒂固的问题加密套件太多协商流程太复杂。TLS 1.2 支持的加密套件数量多得惊人而且很多套件之间存在微妙的兼容性差异。比如 RSA 密钥交换套件很简单客户端拿服务器公钥加密密码即可但缺点是不支持前向保密一旦服务器私钥泄露历史流量全部可被解密。ECDHE 套件则通过临时密钥对实现前向保密安全性更好但配置和协商也稍微复杂。服务器到底用哪一条完全取决于双方清单的交集和顺序。证书验证也是绕不开的环节。服务器发来的证书链可能包含多级证书客户端要从叶子证书逐级验证到根证书检查签名、有效期、吊销状态和扩展字段。有些客户端还会做 OCSP 在线查询这又是一个额外的网络请求虽然通常不会额外占用 TLS 握手 RTT但会因为等待应答而增加握手耗时。所以你会发现TLS 1.2 握手慢不只是慢在网络来回还慢在验证和协商本身的计算与等待。2.4 TLS 1.2 握手的真实开销账本我们算一笔账。假设网络 RTT 是 50 毫秒一个全新 HTTPS 连接需要TCP 握手 1 个 RTTTLS 1.2 握手 2 个 RTTHTTP 请求 1 个 RTT总计 4 个 RTT也就是 200 毫秒。如果网络延迟升高到 100 毫秒总耗时就是 400 毫秒。如果再加上 DNS 解析和证书的 OCSP 在线查询时间会更长。你可能会觉得 200 毫秒不算多但请注意这只是一个请求。一个网页里往往有几十个静态资源虽然浏览器会复用连接但首次连接和会话恢复失效的情况依然很常见。而且在移动网络、跨境访问等弱网场景RTT 可能高达两三百毫秒这时候 2-RTT 的额外开销就很致命了。我记得有一次在远程连一个海外服务本来业务数据只有几十 KBTLS 1.2 握手的耗时却占了整个请求耗时的三分之一优化空间一眼就能看出来。3. TLS 1.3 的减法如何把握手压到一次往返3.1 核心思路把密钥交换参数直接塞进第一条消息TLS 1.3 最大的结构变化是把密钥交换提前到了第一条消息里。客户端的 ClientHello 不再只是声明“我支持哪些套件”而是直接携带一个 ECDHE 密钥交换参数也就是一个临时公钥。服务器收到后不需要再等客户端第二次请求它立刻就能用自己生成的临时公钥和客户端公钥算出共享密钥。于是握手过程变成了这样客户端发 ClientHello里面带 client_random 和客户端公钥服务器回复 ServerHello里面带 server_random、服务器公钥和证书。这一步完成时双方已经能独立计算出同一把会话密钥。后面的 Certificate、CertificateVerify、Finished 消息全部可以加密传输而且服务器在第一次响应里就能把这些加密握手消息全部发完。整个握手只用了 1 个 RTT。这个改进看似只是移动了一个参数的位置实质是改变了握手的顺序模型。TLS 1.2 是“先协商后交换密钥”天然需要两次往返TLS 1.3 是“边协商边交换密钥”把两次往返压缩成一次。就好比以前去药店买药要先跟药师描述症状药师再进库房查药然后告诉你有没有现在你直接把症状写在一张单子上药师看一眼就能把药拿出来。3.2 砍掉老套件把选择困难症根治了TLS 1.3 不只是优化流程还大幅精简了加密套件。它不再支持 RSA 密钥交换只保留基于 ECDHE 的密钥交换方案并且强制使用具备前向保密的 AEAD 对称加密套件具体来说就是 TLS_AES_128_GCM_SHA256、TLS_AES_256_GCM_SHA384 和 TLS_CHACHA20_POLY1305_SHA256。配置简单了误配风险也小了。这套“减法”给性能和安全都带来了好处。以前服务器管理员要在几十种套件里挑组合挑错了可能出现某个老客户端连不上或者安全扫描工具报弱套件告警。TLS 1.3 把选择空间砍到只剩三条大家基本可以闭眼选。从安全角度看废弃 RSA 静态密钥交换意味着即使服务器私钥泄露也无法解密历史流量这就是前向保密的价值。很多安全合规要求现在直接要求禁用 TLS 1.0/1.1 并尽量使用 TLS 1.3原因就在于此。有人会担心砍掉老套件会导致兼容性变差。确实极老的客户端可能不支持 TLS 1.3所以实际部署中通常会让服务器同时开启 TLS 1.2 和 TLS 1.3让老客户端走 TLS 1.2新客户端走 TLS 1.3。这样既保证兼容又能让大多数用户享受 1-RTT 的低延迟。3.3 一次往返的握手消息流长什么样用抓包视角看TLS 1.3 的握手过程清晰很多。客户端发出的第一条 ClientHello 里有三个关键扩展supported_versions 写明支持 TLS 1.3key_share 携带客户端公钥pre_shared_key 或者 psk_key_exchange_modes 用于后续会话恢复。服务器回复的 ServerHello 以 supported_versions 和 key_share 确认参数然后立刻发送 EncryptedExtensions、Certificate、CertificateVerify 和 Finished这些消息从服务器角度看就是一次响应里的连续多个记录层消息。客户端收到后验证证书和 Finished然后发送自己的 Finished同时可以直接开始发送业务数据。严格来说TLS 1.3 的“完整握手”在第一个 RTT 之后就已经可以发送应用数据了。后面客户端发掉的 Finished 更多是礼节性地确认在性能计算上不会再增加一次网络往返。如果你用 openssl 或 Wireshark 观察会看到从 ClientHello 到服务器证书消息之间的时间间隔就是那个典型的 1-RTT 耗时。4. 0-RTT省掉往返的快是要还的4.1 会话票据与 PSK0-RTT 的入场券从哪来TLS 1.3 的完整握手已经压缩到 1-RTT但业界还不满足于是又衍生出了 0-RTT。这个名字听起来很酷但它的实现前提是你之前已经和服务器完成过一次完整握手。在第一次完整握手结束时服务器会下发一个 NewSessionTicket 消息里面包含一张会话票据。这张票据本质上是一段由服务器密钥加密的状态信息也可以理解为服务器给客户端发的一张“会员卡”。客户端下次再连接到同一台服务器时可以在 ClientHello 里带上这张票据双方就可以直接从中恢复出会话密钥不需要再做 ECDHE 密钥交换也不用传输证书。更关键的是客户端可以在第一条消息里就携带业务数据这一部分就叫 Early Data也就是早期数据。这就是 0-RTT 的含义无需等待服务器的任何响应客户端在第一个包就把数据发过去了。0-RTT 的性能优势是实打实的。对于短连接、高频请求的场景比如移动 App 上报日志、查询接口0-RTT 能把原本的建连延迟直接抹掉。但这张“会员卡”有一个微妙的特性它不是身份的绝对证明而是一把可能会被复制的钥匙。4.2 重放攻击0-RTT 的头号风险0-RTT 最大的风险是重放攻击。设想一下客户端在第一个包里带着票据和加密的早期数据发往服务器。这个过程里网络路径上的攻击者虽然解不开加密数据但他可以把整个数据包原样复制一份稍后再次发送给服务器。服务器收到两份相同请求时如果只是简单校验票据合法并直接处理那么这条相同请求就会被处理两次。这里的关键在于0-RTT 数据在服务器端无法像常规握手那样通过 Finished 确认来保证“是同一个新鲜连接”。TLS 1.3 为 0-RTT 设计了一些可选的防重放机制比如要求服务器记录票据使用时间窗口、让票据只能一次性使用、或者要求客户端加入额外随机数。但这些机制只能在服务器端实现而且不同实现方案的效果千差万别。所以官方规范和实际部署都特别强调0-RTT 只适合幂等请求。所谓幂等就是指同一个请求执行一次和执行多次结果是一样的。比如读取一条配置、查询一篇文章、下载一个文件这类操作天然不怕重放。而支付、下单、修改用户信息这类操作一旦被重放可能导致用户被扣两次款或者生成重复订单。这类业务绝对不应该开 0-RTT起码不应该把关键业务放在 Early Data 里。4.3 哪些场景可以开 0-RTT哪些千万别开在实际项目里我对 0-RTT 的态度是能不开就不开开了也绝对不碰核心写操作。适合开 0-RTT 的场景有这几类第一高频且幂等的查询接口比如天气查询、库存查询、推荐列表重放结果没有负面影响。第二短生命周期的小数据上报比如客户端上报一些埋点日志丢了或者重放一次影响可以忽略。第三内部服务之间已经通过其他方式做好幂等处理的调用链路比如数据库层有唯一索引兜底消息队列有去重机制。不适合开 0-RTT 的场景也很明确凡是涉及修改状态的请求都不要放 Early Data。比如用户登录后更新资料、提交订单、上传文件、修改密码。这些操作即使业务层做了幂等一旦在 TLS 层被重放排查起来依然很痛苦。我见过有团队为了追求首屏速度给全站开了 0-RTT结果订单服务偶发重复扣款查了好几天才发现是 TLS 0-RTT 重放导致的问题。还有一点要注意0-RTT 依赖会话票据票据是有有效期的。如果客户端和服务器之间的会话票据过期了客户端尝试 0-RTT 时服务器会拒绝 Early Data让它退回 1-RTT 完整握手。这是正常的落回行为不影响功能但会表现为“第一次连接快过一段时间后第一次连接又慢了”。这类现象并不代表故障只是票据过期后的正常降级。5. 日常排查与调优当握手出问题怎么办5.1 常见 TLS 警告和报错逐个拆实际工作中我经常在浏览器和系统日志里看到几类 TLS 问题这里逐个说说。一类是“协商的 tls 1.0 是非安全协议只有在为了实现向后兼容性才受支持”的警告。这种警告一般出现在你访问一个老旧的服务器或者本机配置里仍然允许 TLS 1.0 时。浏览器能连上但会明确告诉你协议不安全。处理方式很简单服务器端关闭 TLS 1.0/1.1启用 TLS 1.2 和 1.3如果一时改不了服务器也可以在客户端系统层面禁用旧协议但这不是长久之计。所有新的合规要求基本都要求禁用 1.0 和 1.1所以能升级就尽快升级。另一类是“创建 tls 客户端 凭据时发生严重错误内部错误状态为 10013”。这个错误我遇到过一次当时是一个 Windows 服务调用 HTTPS 接口突然报错。10013 对应的底层含义是权限被拒绝单独看数字像是系统套接字错误但结合“TLS 客户端凭据”这个场景常见原因有三个一是证书私钥对应的访问权限不足服务账户没有权限读取私钥二是本机安全策略或防火墙规则拦截了发起连接所需的端口三是系统时间与服务器偏差过大导致证书验证失败后被当成严重错误抛出。排查顺序建议是先确认服务账户是否有证书私钥的读取权限再看端口出方向策略最后看系统时间和证书有效期。这类错误虽然吓人但大部分情况下不是 TLS 协议本身坏了而是外围权限或环境配置出了问题。还有一种很常见的是“证书链不完整”或“无法找到证书颁发者”。这通常发生在服务器把证书链配置全的情况下比如只配置了叶子证书没有配置中间证书。客户端用叶子证书没法直接连到它信任的根证书于是验证失败。处理方式很简单把中间证书补进服务器的证书链文件。其实很多 HTTPS 报错追到底都是证书配置问题而不是密码算法问题。5.2 用 openssl 和 curl 看你的握手细节排查 TLS 问题我首先会用到 openssl。命令很简单openssl s_client -connect example.com:443 -servername example.com -tls1_3 -brief这段命令会输出协议版本、协商出的加密套件、会话 ID 等信息。想看得更详细把-brief去掉改成-debug或者-state就能看到握手的每一阶段状态变化比如是否收到了服务器证书、是否发送了 ClientKeyExchange、Finished 是否通过。想看服务器支持哪些协议版本可以分多次指定-tls1_2、-tls1_1、-tls1逐个测试。curl 也是我最常用的工具尤其是看延迟分布。可以这样curl -o /dev/null -s -w time_connect: %{time_connect}\ntime_appconnect: %{time_appconnect}\ntime_starttransfer: %{time_starttransfer}\n https://example.com/其中time_connect是 TCP 连接完成的时间点time_appconnect是 TLS 握手完成的时间点两者之差就是 TLS 握手耗时。time_starttransfer是收到第一个字节的时间点用它减去time_appconnect可以大致估算业务响应耗时。如果time_appconnect比time_connect大很多说明 TLS 握手拖了后腿如果两者差距很小说明多半是 TLS 1.3 或者会话复用生效了。需要提醒一句用 openssl 或 curl 测试时本机和服务器之间的网络情况可能和真实用户完全不一样尤其是跨地域场景。所以看到一次握手延迟很低不要直接得出“线上没问题”的结论要多测几个节点或者用真实用户抓包数据来验证。5.3 我调优线上服务时的一些实操顺序调优 TLS 性能我的习惯是严格按顺序来先看版本再看证书再想会话复用。第一步确认服务器已经启用 TLS 1.2 和 TLS 1.3关闭 1.0/1.1。这一步能解决大部分安全警告也是性能优化的基础。第二步确认证书链完整、OCSP 装订已开启。OCSP Stapling 能让服务器自己把证书的吊销状态证明随握手发过来省去客户端额外查询对 TLS 1.2 尤其有效。第三步检查会话复用是否生效。TLS 1.3 的会话票据默认是支持的但有些网关或者负载均衡器会关闭这会导致每个连接都走完整握手。如果业务是频繁短连接开启会话复用能极大降低建连开销。第四步才是考虑 0-RTT。而且只在确认业务幂等、安全团队同意之后才会开。我个人的原则是读多写少的服务可以考虑写操作多或者有安全合规压力的服务坚决不开。最后每次调整完 TLS 配置都要做回归测试重点看两类客户端一类是老旧系统比如 Windows 7 或者旧版安卓 WebView另一类是自动化脚本、监控系统、爬虫程序它们可能对 TLS 版本和套件特别敏感。我曾经改完 TLS 配置页面正常但监控系统开始报错就是因为监控服务器上的 OpenSSL 版本太老不支持新的套件。实际操练下来你可能会发现TLS 性能优化里最关键的往往不是那几个 RTT 的算法差异而是部署配置的细节。协议版本、证书链、会话复用、OCSP、连接复用每一样都比纠结 0-RTT 更值得先处理好。把这些基础做扎实绝大多数 HTTPS 慢的问题都能解决。至于 0-RTT它更像是一个锦上添花的加速选项而不是一个默认就该打开的开关。