HTTP 协议基础与版本演进
本节目标
- 掌握常见请求方法与状态码的真实含义
- 理解 HTTPS 的「非对称传密钥 + 对称传数据」机制
- 用一段 Node 代码实测 HTTP/1.1 与 HTTP/2 的建连差异
请求方法:GET(读取)、POST(新建/提交)、PUT(整体替换)、PATCH(局部更新)、DELETE(删除)、HEAD(只要响应头不要体)、OPTIONS(预检/探测)。注意:方法只是「语义约定」,服务器想怎么实现都行,但大家都遵守约定才有意义。
常见状态码(按首位记忆最轻松):
2xx 成功 200 正常 | 201 已创建
3xx 重定向 301 永久 | 302 临时 | 304 缓存生效(没变)
4xx 客户端错 400 参数错 | 401 未认证 | 403 无权限 | 404 不存在 | 405 方法不允许
5xx 服务器错 500 内部错 | 502 网关错 | 503 不可用 | 504 网关超时HTTPS 加密流程(TLS 握手,简化版):
① 客户端打招呼:带上支持的加密套件 + 随机数 A
② 服务器返回证书(含公钥)+ 随机数 B
③ 客户端验证证书(走 CA 信任链)→ 用公钥加密一个「预主密钥」发给服务器
④ 双方用 随机数A + 随机数B + 预主密钥 各自算出相同的「会话密钥」(对称密钥)
⑤ 之后所有通信都用这个对称密钥加密(快)要点:非对称加密只用来安全地传「密钥」;真正传数据用对称加密(快);证书解决「你拿到的公钥到底是不是真的服务器」。
版本演进对比:
| 版本 | 痛点 | 改进 |
|---|---|---|
| 1.1 | 队头阻塞(一个 TCP 连接同一时刻只处理一个请求) | 持久连接 + 管线化(有限) |
| 2 | 应用层队头阻塞 | 二进制分帧、多路复用、头部压缩、服务端推送 |
| 3 | TCP 层队头阻塞(丢包阻塞整条连接) | 基于 QUIC(UDP)、0-RTT 握手、连接迁移 |
面试常问:HTTP/2 都多路复用了,为什么还有队头阻塞?→ 因为多路复用是在应用层,底层还是一条 TCP;TCP 丢一个包,整条连接都要等重传,所以 HTTP/3 干脆换成 UDP 的 QUIC。
下面这段 Node 代码会实测 HTTP/2 的多路复用建连与 HTTP/1.1 的差异(需 Node 18+,HTTP/2 为内置模块):
// 运行环境:Node.js 18+
// 步骤:node brw-l3.js
// 分别用 HTTP/1.1 和 HTTP/2 请求一个支持 h2 的站点,并演示 h2 的「单连接多路复用」
const https = require('node:https')
const http2 = require('node:http2')
const target = 'https://www.cloudflare.com' // Cloudflare 同时支持 h1/h2/h3
let h1Done = false
let h2Done = false
const tryExit = () => { if (h1Done && h2Done) process.exit(0) }
// —— HTTP/1.1:每个请求通常新建一条连接(agent:false 让连接用完即关)——
https.get(target + '/', { agent: false }, (res) => {
console.log('HTTP/1.1 协议版本:', res.httpVersion, '状态:', res.statusCode)
res.resume()
res.on('end', () => { h1Done = true; tryExit() })
}).on('error', (e) => { console.log('h1 失败:', e.message); h1Done = true; tryExit() })
// —— HTTP/2:多条请求复用同一条 TCP 连接(多路复用)——
const session = http2.connect(target)
session.on('error', (e) => { console.log('h2 会话失败:', e.message); h2Done = true; tryExit() })
const paths = ['/', '/cdn-cgi/trace', '/robots.txt'] // 同一连接上并发发 3 个请求
let done = 0
const finish = () => {
if (++done === paths.length) session.close() // 全部完成再关闭这条 h2 连接
}
for (const p of paths) {
const req = session.request({ ':path': p })
req.resume() // 消费响应体,否则 'end' 不会触发、连接无法关闭
req.on('response', (headers) => {
console.log(
'HTTP/2 协议: h2 | 路径', p, '| 状态:', headers[':status'],
'(多路复用:单连接并发', paths.length, '个请求)'
)
})
req.on('end', finish)
req.on('error', (e) => { console.log('h2 请求失败:', e.message); finish() })
req.end()
}
// h2 会话真正关闭后再标记完成,避免 h1 keep-alive 长连接吊住进程
session.on('close', () => { h2Done = true; tryExit() })名词解释
状态码(Status Code):服务器用三位数字告诉客户端「这次请求结果如何」。它的「方法」就是按首位分类:2=成功、3=重定向、4=你(客户端)的问题、5=我(服务器)的问题。记住这个分类法,看到任意新状态码都能先猜出大致含义。
对称加密(Symmetric Encryption):加密和解密用同一把密钥。优点是快,缺点是「怎么把密钥安全交给对方」是个难题。常见算法:AES。
非对称加密(Asymmetric Encryption):有一对密钥——公钥(谁都能拿)和私钥(只有自己有)。用公钥加密的数据,只有私钥能解开。优点是「不用提前共享密钥」,缺点是慢。常见算法:RSA、ECC。HTTPS 用公钥加密传「会话密钥」,之后切到 AES 对称加密,是「扬长避短」的经典组合。
课后练习
练习 1:用户在浏览器地址栏输入 http://example.com/login 后回车,服务器返回 301 到 https://example.com/login。请描述浏览器接下来会怎么做,以及 301 和 302 的区别。
答案:浏览器收到 301(永久重定向)后,会自动用
https重新请求目标地址;后续它甚至会「记住」这个永久跳转,下次直接发 HTTPS。区别:301 是「永久搬家」,浏览器/搜索引擎会更新书签和权重;302 是「临时去别处」,不会长期记住。对http→https强制升级,用 301 更合适。
练习 2:有人说「HTTPS 比 HTTP 慢,所以内网服务没必要上 HTTPS」。你同意吗?说说你的判断。
答案:要看场景。对内网、纯后端微服务间通信,若网络可信且没法律/合规要求,HTTP 确实少一次握手开销;但现代 TLS 握手成本已很低,且内网也未必「可信」(横向移动攻击)。更稳妥的做法是内网也上 TLS(或用服务网格自动 mTLS)。所以「没必要」这句话太绝对——是否上 HTTPS 应基于「威胁模型」而非「怕慢」。
总结
我越来越觉得,HTTP 协议的学习重点不是「背状态码数字」,而是理解每一代协议都在和同一个敌人作战:延迟。1.1 败给队头阻塞,2 在应用层赢了但底层 TCP 仍拖后腿,3 直接掀桌换 UDP。而 HTTPS 又叠加了一层「安全握手」的开销——安全和速度天然有张力。真正成熟的工程师不会简单地说「HTTPS 慢所以不用」,而是会算账:握手慢多少毫秒、值不值得换来的安全、能不能用连接复用/0-RTT 把这部分开销吃回来。协议演进史,本质上是一部「在延迟、安全、复杂性之间反复权衡」的历史。