HTTP 与 HTTPS 详解:从三次握手到 TLS 1.3 的完整链路
一次”网站打不开”,牵出了七层协议
去年帮同事排查一个诡异问题:网站在公司的 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 天然适合做缓存、做代理、做负载均衡——中间设备只要看懂请求和响应,就能决定要不要拦下来。
无状态,以及 Cookie 怎么补的位
HTTP 是无状态的:服务器不会记住你上一次请求做了什么。第 2 次请求对服务器来说,跟第 1 次请求毫无关系。
这么设计是为了让服务器好扩展——任何一台机器都能处理任何请求,不用同步会话状态。但现实是登录态必须记住,于是有了 Cookie:
- 登录后服务器在响应里塞一个
Set-Cookie: sessionid=abc123 - 浏览器把这个 Cookie 存起来
- 之后每次请求自动带上
Cookie: sessionid=abc123 - 服务器靠这个 sessionid 去查会话数据
也就是说,状态其实被挪到了客户端保存。这带来了一个直接后果:Cookie 会自动跟着请求发出去,也就成了 CSRF 攻击的载体,所以后来才有了 SameSite 属性这个补丁。
从输入 URL 到页面出现:完整流程
把上面这些串起来,一次 HTTPS 请求在浏览器地址栏敲下回车后,实际经历的是这样一条链路:
图里第 8 步之后往往还有第 4~6 步的循环——页面里的每个 CSS、JS、图片都要单独请求一次,一个普通网页动辄几十上百个请求。
HTTP 跑在 TCP 之上
HTTP 自己不管数据怎么可靠送达,它把报文交给 TCP,TCP 负责把字节流完整、有序地送到对端。所以每次 HTTP 通信之前,都必须先建立 TCP 连接。
TCP 三次握手
建立连接要三个包来回,目的是让双方都确认”你能收到我发的、我能收到你发的”。
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。
状态码
| 状态码 | 含义 | 典型场景 |
|---|---|---|
| 200 | OK | 请求成功 |
| 201 | Created | 资源创建成功 |
| 204 | No Content | 成功但没有响应体 |
| 301 | Moved Permanently | 永久重定向,SEO 权重转移 |
| 302 | Found | 临时重定向 |
| 304 | Not Modified | 缓存还有效,用本地副本 |
| 400 | Bad Request | 请求参数有问题 |
| 401 | Unauthorized | 没登录或 token 失效 |
| 403 | Forbidden | 登录了但没权限 |
| 404 | Not Found | 资源不存在 |
| 405 | Method Not Allowed | 方法不被支持 |
| 429 | Too Many Requests | 触发限流 |
| 500 | Internal Server Error | 服务端代码抛异常 |
| 502 | Bad Gateway | 网关拿不到上游响应 |
| 503 | Service Unavailable | 服务不可用,通常过载或维护 |
| 504 | Gateway 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 从协议层面解决了这个问题:
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 的解法是两个都用:
TLS 1.2 握手:两个 RTT
握手就是”把上面那套密钥协商流程走一遍”。TLS 1.2 的完整握手需要两个 RTT(往返时延):
TLS 1.3 握手:一个 RTT
TLS 1.2 那两个 RTT,很大一部分浪费在”先商量用哪种密钥交换算法,再交换密钥”上。TLS 1.3 的思路很直接:别商量了,我把所有可能的公钥份额一次性全发过去。
TLS 1.3 还砍掉了一批不安全的老算法(RC4、SHA-1、静态 RSA 密钥交换、CBC 模式),只保留 AEAD 套件。一个重要的副作用是:TLS 1.3 默认提供前向安全——即使服务器私钥将来泄露,过去的通信记录也解不开。
证书与信任链
客户端凭什么相信证书里的公钥真的属于这个域名?靠的是信任链:
- 操作系统和浏览器内置了一批根 CA 的证书(根证书)
- 根 CA 给中间 CA 签名,中间 CA 再给你的网站签名
- 客户端拿到网站的证书后,逐级往上验签,一直验到内置的根证书
- 验签通过,且证书里的域名与正在访问的域名匹配、证书在有效期内、没被吊销——才认定对方身份可信
想看看一个证书里到底写了什么,可以用本站的证书解析工具。
请求头与响应头速查
日常调试最常打交道的几个头,可以用本站的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-Origin | CORS 允许的源 |
Strict-Transport-Security | HSTS,强制后续访问走 HTTPS |
Content-Security-Policy | CSP,限制资源加载来源防 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.1 | HTTPS (TLS 1.2) | HTTP/2 | HTTP/3 | |
|---|---|---|---|---|
| 传输层 | TCP | TCP + TLS | TCP + TLS | QUIC(UDP) |
| 报文格式 | 文本 | 加密文本 | 二进制分帧 | 二进制分帧 |
| 连接复用 | 串行排队 | 串行排队 | 多路复用 | 多路复用 |
| 队头阻塞 | 应用层严重 | 应用层严重 | 应用层解决 | 传输层也解决 |
| 握手开销 | 1 RTT | 2~3 RTT | 2~3 RTT | 1 RTT(复用 0-RTT) |
| 头部压缩 | 无 | 无 | HPACK | QPACK |
| 必须加密 | 否 | 是 | 事实上是 | 是(内置) |
常见问题
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。