D
开发工具箱

HTTP 与 HTTPS 详解:从三次握手到 TLS 1.3 的完整链路

网络工具 2026年8月30日 约 1 分钟阅读

一次”网站打不开”,牵出了七层协议

去年帮同事排查一个诡异问题:网站在公司的 WiFi 下打不开,换成手机流量就好了。他第一反应是”服务器挂了”,我让他打开浏览器开发者工具看了一眼——请求卡在 TLS 握手阶段,服务端证书链不完整,公司网络的中间设备校验证书失败就直接掐断了连接。

这件事挺能说明问题:用户眼里的”打开一个网页”,底下是 DNS、TCP、TLS、HTTP 四套协议接力跑完的结果。任何一环出问题,表现都是同一个——白屏。

这篇就把这条链路从头到尾捋一遍,把 HTTP 和 HTTPS 到底在干什么讲清楚。

先说清楚:HTTP 到底是什么

HTTP(HyperText Transfer Protocol,超文本传输协议)是应用层协议,规定了客户端和服务器之间对话的格式。它只管两件事:客户端怎么问,服务器怎么答。至于数据怎么从一台机器传到另一台机器,那不是 HTTP 的活,是 TCP 的活。

请求-响应模型

HTTP 永远是客户端先开口。一次交互就是一问一答:

客户端:GET /api/user?id=42 HTTP/1.1
        Host: example.com
        Accept: application/json

服务器:HTTP/1.1 200 OK
        Content-Type: application/json
        Content-Length: 38

        {"id": 42, "name": "张三", "vip": true}

服务器不会主动给客户端推消息(HTTP/2 的 Server Push 和 WebSocket 是另一回事)。这个单向发起的特性,让 HTTP 天然适合做缓存、做代理、做负载均衡——中间设备只要看懂请求和响应,就能决定要不要拦下来。

HTTP 是无状态的:服务器不会记住你上一次请求做了什么。第 2 次请求对服务器来说,跟第 1 次请求毫无关系。

这么设计是为了让服务器好扩展——任何一台机器都能处理任何请求,不用同步会话状态。但现实是登录态必须记住,于是有了 Cookie:

  1. 登录后服务器在响应里塞一个 Set-Cookie: sessionid=abc123
  2. 浏览器把这个 Cookie 存起来
  3. 之后每次请求自动带上 Cookie: sessionid=abc123
  4. 服务器靠这个 sessionid 去查会话数据

也就是说,状态其实被挪到了客户端保存。这带来了一个直接后果:Cookie 会自动跟着请求发出去,也就成了 CSRF 攻击的载体,所以后来才有了 SameSite 属性这个补丁。

从输入 URL 到页面出现:完整流程

把上面这些串起来,一次 HTTPS 请求在浏览器地址栏敲下回车后,实际经历的是这样一条链路:

客户端 服务端 1 输入 URL 浏览器解析出协议、域名、端口、路径 2 DNS 服务器 查询域名 IP 返回 IP 地址 3 ① SYN seq=x ② SYN+ACK seq=y, ack=x+1 ③ ACK ack=y+1 TCP 三次握手完成,连接建立 4 ClientHello / 证书 / 密钥交换 ServerHello / ChangeCipherSpec / Finished (仅 HTTPS 需要,详见后文 TLS 握手一节) 5 GET /index.html HTTP/1.1 6 服务端处理 应用逻辑、查库、渲染模板 7 HTTP/1.1 200 OK + HTML 内容 8 浏览器渲染 解析 HTML/CSS/JS,遇到新资源则重复第 4~6 步 9 FIN ACK + FIN + ACK 四次挥手关闭连接;若是 Keep-Alive 则复用连接跳过 2、3、8
图 1:从输入 URL 到页面渲染的完整链路。DNS 解析拿到 IP,TCP 三次握手建立连接,HTTPS 还要额外做一次 TLS 握手,之后才轮到 HTTP 请求与响应。点上面的「播放」可以看每一步。

图里第 8 步之后往往还有第 4~6 步的循环——页面里的每个 CSS、JS、图片都要单独请求一次,一个普通网页动辄几十上百个请求。

HTTP 跑在 TCP 之上

HTTP 自己不管数据怎么可靠送达,它把报文交给 TCP,TCP 负责把字节流完整、有序地送到对端。所以每次 HTTP 通信之前,都必须先建立 TCP 连接

TCP 三次握手

建立连接要三个包来回,目的是让双方都确认”你能收到我发的、我能收到你发的”。

客户端 服务端 CLOSED LISTEN ① SYN=1, seq=x SYN-SENT ② SYN=1, ACK=1, seq=y, ack=x+1 SYN-RCVD ③ ACK=1, seq=x+1, ack=y+1 ESTABLISHED ESTABLISHED HTTP 请求 / 响应数据 两次握手不够:服务端无法确认自己的初始序号已被对方收到
图 2:TCP 三次握手。三个包分别确认「客户端能发」「服务端能收且能发」「客户端能收」,双方各自同步初始序列号后进入 ESTABLISHED。点上面的「播放」可以看状态是怎么一步步迁移的。

TCP 四次挥手

断开连接比建立连接麻烦,因为TCP 是全双工的,两个方向要分别关闭。

客户端(主动关闭) 服务端(被动关闭) ESTABLISHED ESTABLISHED ① FIN=1, seq=u 主动方进入 FIN-WAIT-1 ② ACK=1, ack=u+1 被动方 CLOSE-WAIT,主动方 FIN-WAIT-2 半关闭:被动方仍可继续发送剩余数据 ③ FIN=1, seq=v(剩余数据已发完) 被动方进入 LAST-ACK ④ ACK=1, ack=v+1 被动方 CLOSED,主动方进入 TIME-WAIT TIME-WAIT 要等 2MSL(约 1~4 分钟)后才 CLOSED ① 保证最后的 ACK 能到达对端 ② 让本次连接的残留报文在网络中消散
图 3:TCP 四次挥手。因为 TCP 是全双工的,两个方向必须分别关闭,所以第二次和第三次挥手之间还夹着一段「被动方继续发剩余数据」的半关闭状态。点上面的「播放」可以看完整过程。

为什么握手三次、挥手却是四次

建立连接时,服务端收到 SYN 后可以把自己的 SYN 和对客户端的 ACK 合并成一个包发出去,所以只需要三次。

关闭连接时不行:服务端收到 FIN 后,可能还有数据没发完,只能先回一个 ACK 表示”我知道你要关了”,等数据发完了才能再发自己的 FIN。这一拆,就多了一次。

顺带一提,服务器上出现大量 CLOSE_WAIT 通常意味着应用代码忘了调用 close() 关闭 socket,连接卡在半关闭状态出不来。这是后端排查连接泄漏时最常见的一个信号。

HTTP 报文长什么样

HTTP 报文是纯文本(HTTP/2 之后改成了二进制帧,但语义没变),结构上分为三块。

请求报文

POST /api/login HTTP/1.1          ← 请求行:方法 + 路径 + 版本
Host: example.com                 ← 请求头开始
Content-Type: application/json
Authorization: Bearer eyJhbGci...
User-Agent: Mozilla/5.0 ...
Content-Length: 41
                                  ← 空行,头和体在此分界
{"username": "zhangsan", "pwd": "..."}   ← 请求体

响应报文

HTTP/1.1 201 Created              ← 状态行:版本 + 状态码 + 原因短语
Content-Type: application/json    ← 响应头开始
Set-Cookie: sessionid=abc123; HttpOnly; SameSite=Lax
Cache-Control: no-store
Content-Length: 28
                                  ← 空行
{"ok": true, "userId": 42}        ← 响应体

那个空行是硬性规定:头部和体之间必须是 CRLF CRLF。很多手搓 HTTP 解析的 bug 都出在这。

常用方法

方法语义幂等常用场景
GET获取资源查询、页面、静态文件
POST提交数据登录、下单、创建资源
PUT完整替换资源整体更新用户信息
PATCH部分更新资源只改某个字段
DELETE删除资源删除
HEAD只要响应头探测资源是否存在、看大小
OPTIONS询问支持的方法CORS 预检请求

幂等的意思是:同一个请求执行一次和执行多次,服务端状态一样。支付接口如果设计成 GET,被浏览器预加载或爬虫碰一下就多扣一笔钱——这就是为什么创建订单必须用 POST。

状态码

状态码含义典型场景
200OK请求成功
201Created资源创建成功
204No Content成功但没有响应体
301Moved Permanently永久重定向,SEO 权重转移
302Found临时重定向
304Not Modified缓存还有效,用本地副本
400Bad Request请求参数有问题
401Unauthorized没登录或 token 失效
403Forbidden登录了但没权限
404Not Found资源不存在
405Method Not Allowed方法不被支持
429Too Many Requests触发限流
500Internal Server Error服务端代码抛异常
502Bad Gateway网关拿不到上游响应
503Service Unavailable服务不可用,通常过载或维护
504Gateway Timeout网关等上游超时

401 和 403 的区别值得记一下:401 是”你是谁”,403 是”我知道你是谁,但你不能碰”

HTTP 的三次进化

HTTP/1.1:持久连接

HTTP/1.0 每发一个请求都要新建一条 TCP 连接,用完就关。建一次连接的成本是三次握手加一次慢启动,非常浪费。

HTTP/1.1 默认开启 Connection: keep-alive,一条连接可以复用多次请求。但它有个硬伤:同一条连接上请求必须串行,前一个响应没回来,后面的请求只能干等。

HTTP/2:多路复用

一个页面通常要加载几十个资源。串行等待的延迟实在受不了,浏览器的土办法是同时开 6~8 条 TCP 连接绕过限制——但每条连接都要握手,服务端压力也大。

HTTP/2 从协议层面解决了这个问题:

浏览器要取 3 个资源 HTTP/1.1(队头阻塞) HTTP/2(多路复用) 请求1 响应1 等待 请求2 响应2 等待 请求3 响应3 请求1 响应1 请求2 响应2 请求3 响应3 3 条流共用 1 条连接,交错传输 时间 时间 HTTP/2 单连接多路复用,解决了应用层队头阻塞 HTTP/3 改用 QUIC(基于 UDP),连 TCP 层的队头阻塞也一并消除
图 4:HTTP/1.1 与 HTTP/2 的请求时序对比。左边请求必须排队等前一个响应,右边三条流在同一条连接上并行交错。点上面的「播放」可以看两者的差异。

HTTP/2 还顺带做了头部压缩(HPACK)和二进制分帧。值得注意的是,它虽然叫 HTTP/2,但在浏览器里必须配合 HTTPS 使用——所有主流浏览器都只支持 h2(基于 TLS),不支持明文版 h2c

HTTP/3:QUIC

HTTP/2 解决了应用层的队头阻塞,但 TCP 层的还在:TCP 把数据看成单一字节流,丢一个包,后面所有数据都得等重传,不管它属于哪个流。

HTTP/3 干脆把传输层换成了 QUIC——跑在 UDP 之上,自己实现可靠传输和拥塞控制,并且按流做重传。一条流丢包不会影响其他流。代价是 UDP 在部分企业网络和中间设备上会被限速甚至阻断,部署时需要额外考虑回退到 HTTP/2。

HTTPS:给 HTTP 套一层 TLS

HTTPS 不是新协议,它是 HTTP over TLS——把 HTTP 报文交给 TLS 加密后再交给 TCP 传输。

它要解决三个问题

威胁例子HTTPS 的对策
窃听公共 WiFi 抓包看到你的密码加密,中间人只能看到密文
篡改运营商往页面里插广告完整性校验,改一个字节就会被发现
冒充DNS 劫持到假银行网站证书验证,证明对方确实是域名的持有者

混合加密:既安全又快

这里有个看似矛盾的地方:对称加密快但密钥不好传,非对称加密能安全传密钥但慢。HTTPS 的解法是两个都用

客户端 服务端 ① 证书(内含服务器公钥) 非对称加密:安全但慢 ② 用公钥加密的 Pre-Master Secret 只有持有私钥的服务端能解开 算出会话密钥 算出会话密钥 双方各算一次,得到同一把密钥 ③ 会话密钥加密的应用数据 对称加密跑完全场:非对称只在开头用一次
图 5:HTTPS 的混合加密。非对称加密负责安全地协商出一把会话密钥,之后的数据传输全部交给对称加密。点上面的「播放」可以看密钥是怎么协商出来的。

TLS 1.2 握手:两个 RTT

握手就是”把上面那套密钥协商流程走一遍”。TLS 1.2 的完整握手需要两个 RTT(往返时延):

客户端 服务端 ① ClientHello:TLS 版本 / 密码套件 / 随机数 R1 ② ServerHello:选定套件 + 随机数 R2 ③ Certificate:证书链(含公钥) ④ ServerHelloDone 验证证书 检查信任链 / 域名匹配 / 有效期 / 吊销状态 生成 Pre-Master Secret ⑤ ClientKeyExchange:公钥加密的 Pre-Master 算出会话密钥 算出会话密钥 R1 + R2 + Pre-Master → Master Secret → 会话密钥 ⑥ ChangeCipherSpec + Finished ⑦ ChangeCipherSpec + Finished 握手完成(2-RTT),之后的应用数据全部对称加密传输
图 6:TLS 1.2 的完整握手。双方交换随机数、验证证书、用非对称加密传递 Pre-Master Secret,各自推导出会话密钥,最后用 Finished 互相确认握手没被篡改。点上面的「播放」可以看每一步。

TLS 1.3 握手:一个 RTT

TLS 1.2 那两个 RTT,很大一部分浪费在”先商量用哪种密钥交换算法,再交换密钥”上。TLS 1.3 的思路很直接:别商量了,我把所有可能的公钥份额一次性全发过去

客户端 服务端 ① ClientHello:套件列表 + key_share(公钥份额) 一次性把可能的密钥交换算法全带上,省掉一轮 ② ServerHello + key_share + 加密的证书 + Finished 服务端一次回完,客户端此时已能算出会话密钥 验证 + 算密钥 ③ Finished(已加密) ④ 应用数据(对称加密) TLS 1.3 只需 1-RTT,比 1.2 省一半 会话复用时 0-RTT,数据可随 ClientHello 直接发出(有重放风险,仅适幂等请求)
图 7:TLS 1.3 的握手。客户端在第一个包里就把密钥交换材料带上,服务端一次回完,握手压缩到 1 个 RTT。点上面的「播放」可以看它比 1.2 省在哪。

TLS 1.3 还砍掉了一批不安全的老算法(RC4、SHA-1、静态 RSA 密钥交换、CBC 模式),只保留 AEAD 套件。一个重要的副作用是:TLS 1.3 默认提供前向安全——即使服务器私钥将来泄露,过去的通信记录也解不开。

证书与信任链

客户端凭什么相信证书里的公钥真的属于这个域名?靠的是信任链

  1. 操作系统和浏览器内置了一批根 CA 的证书(根证书)
  2. 根 CA 给中间 CA 签名,中间 CA 再给你的网站签名
  3. 客户端拿到网站的证书后,逐级往上验签,一直验到内置的根证书
  4. 验签通过,且证书里的域名与正在访问的域名匹配、证书在有效期内、没被吊销——才认定对方身份可信

想看看一个证书里到底写了什么,可以用本站的证书解析工具

请求头与响应头速查

日常调试最常打交道的几个头,可以用本站的HTTP 请求头查看工具直接看到浏览器实际发出了什么。

请求头

作用
Host目标域名。一台服务器托管多个站点就靠它区分
User-Agent客户端标识。想看看自己的 UA 里藏了多少信息,用 UA 解析
Accept / Accept-Encoding / Accept-Language内容协商:我能接受什么格式、什么压缩、什么语言
Authorization身份凭证,通常是 Bearer <token>
Cookie会话标识,浏览器自动带上
Referer从哪个页面跳过来的(拼写错误已成历史遗留)
Origin发起请求的源,CORS 判断的关键
Cache-Control客户端希望的缓存行为
If-None-Match / If-Modified-Since条件请求,命中则返回 304

响应头

作用
Content-Type响应体格式,必须带 charset 否则可能乱码
Content-Length / Transfer-Encoding响应体长度;分块传输用 chunked
Cache-Control缓存策略,max-age / no-store / immutable
ETag / Last-Modified配合条件请求实现 304
Set-Cookie下发 Cookie,可带 HttpOnly / Secure / SameSite
Access-Control-Allow-OriginCORS 允许的源
Strict-Transport-SecurityHSTS,强制后续访问走 HTTPS
Content-Security-PolicyCSP,限制资源加载来源防 XSS

安全相关的头配得对不对,可以用安全头检测扫一遍。

常见误区

误区一:HTTPS 会拖慢网站

早年是,现在基本不会。TLS 握手多做 1~2 个 RTT,但会话复用和 TLS 1.3 已经把它压到 1 个 RTT,现代 CPU 做对称加密的开销可以忽略。反过来,没有 HTTPS 你就用不了 HTTP/2 和 HTTP/3,总体上 HTTPS 反而更快

误区二:用了 HTTPS 就不用管别的了

HTTPS 只保证传输通道安全,不保证应用本身安全。SQL 注入、XSS、越权访问,这些跟 HTTPS 一点关系都没有。它防的是链路上的中间人,不是应用层的漏洞。

误区三:证书验证了,对方就一定可信

证书只证明”对方持有这个域名的私钥”,不证明”这个域名是正经网站”。钓鱼网站一样能申请到合法证书。现在的浏览器会在地址栏淡化 HTTPS 标识,就是这个原因——它早就不该被当成”安全”的同义词。

误区四:GET 和 POST 的区别是长度限制

HTTP 规范对 GET 的 URL 长度没有任何限制,所谓 2KB 限制是老浏览器和服务器的实现约束。真正的区别在语义:GET 是安全且幂等的读操作,POST 是会改变服务端状态的写操作。这个语义决定了 GET 请求可能被预加载、被浏览器历史记住、被爬虫抓走——所以别用 GET 做删除操作。

HTTP vs HTTPS vs HTTP/2 vs HTTP/3

HTTP/1.1HTTPS (TLS 1.2)HTTP/2HTTP/3
传输层TCPTCP + TLSTCP + TLSQUIC(UDP)
报文格式文本加密文本二进制分帧二进制分帧
连接复用串行排队串行排队多路复用多路复用
队头阻塞应用层严重应用层严重应用层解决传输层也解决
握手开销1 RTT2~3 RTT2~3 RTT1 RTT(复用 0-RTT)
头部压缩HPACKQPACK
必须加密事实上是是(内置)

常见问题

Q: HTTPS 握手到底多了几次往返?

TLS 1.2 是 2 个 RTT(加上 TCP 握手共 3 个),TLS 1.3 压到 1 个 RTT(加 TCP 共 2 个)。会话复用时两者都能再省一次:TLS 1.2 会话复用是 1-RTT,TLS 1.3 的 0-RTT 则可以把应用数据和 ClientHello 一起发出去。

Q: 为什么抓包看不到 HTTPS 的内容?

因为你看到的是 TLS 加密后的密文。想看明文需要让客户端信任你的代理证书(Charles、Fiddler、mitmproxy 就是这个原理),或者配置 SSLKEYLOGFILE 让浏览器导出会话密钥给 Wireshark 解密。

Q: 证书过期会怎样?

浏览器直接拦截访问,显示”您的连接不是私密连接”,用户必须手动点”继续”才能进——实际上等于网站挂了。所以现在普遍用 Let’s Encrypt 配自动化续期,把证书有效期压到 90 天。

Q: 什么叫前向安全,为什么重要?

假设攻击者现在把服务器私钥偷走了。如果密钥交换用的是静态 RSA(TLS 1.2 支持,但已不推荐),他就能解开以前录制的所有加密流量;如果用 ECDHE 这类临时密钥交换(TLS 1.3 强制),每次会话的密钥都是临时生成的、用完即弃,私钥泄露也解不开历史流量。这个性质就叫前向安全。

Q: 本地开发怎么上 HTTPS?

mkcert 生成本地可信证书最省事,它会在系统信任库里装一个本地 CA。也可以用 RSA 密钥对生成工具 造自签名证书,只是浏览器会报警告,需要手动信任或加启动参数忽略。

Q: 三次握手能不能携带数据?

可以。TCP Fast Open(TFO)允许在第一个 SYN 包里带数据,能省掉一个 RTT。但因为它存在安全性和中间设备兼容性问题,实际部署比例不高,Linux 上默认也是关闭的。

Q: 为什么服务器上有大量 TIME_WAIT?

TIME_WAIT 出现在主动关闭连接的一方。如果一台机器上有大量 TIME_WAIT,说明它在频繁地主动断开连接——常见于压测客户端、爬虫,或者没启用 Keep-Alive 的短连接服务。解决办法是让对端主动关闭、开启连接复用,或者调 net.ipv4.tcp_tw_reuse