Appearance
HTTP 与 HTTPS
概念
HTTP(HyperText Transfer Protocol,超文本传输协议)是应用层最核心的协议:客户进程发出"请求报文",服务器回"响应报文",一次交互就完成一次事务。 它有两句话必须记住:
| 特性 | 含义 | 后果 |
|---|---|---|
| 无状态(stateless) | 服务器不记"上一次是谁问的" | 好处:服务器简单、可水平扩展;坏处:需要 Cookie 来补状态 |
| 面向文本 | 请求行与首部都是可读的 ASCII 字符串 | 调试友好(用 curl 就能看);也意味着首部本身占不少字节 |
★ HTTPS 不是新协议,而是"HTTP 报文装进 TLS 通道里":HTTPS = HTTP + TLS/SSL——它解决的是 HTTP 明文传输带来的三个问题:被窃听、被篡改、被冒充。
★ 本章的计算核心是"连接方式与 RTT 账":同一个页面(1 个 HTML + 3 张图),用不同的连接方式,端到端时延能从 8 个 RTT 压到 3 个 RTT——这是 408 最爱的应用层计算题。
与上一章的衔接:上一章(net/50-dns.md)算过"DNS 用 UDP 是为了省掉 1 个 RTT 的握手"——本章的账正好反过来:HTTP 必须建 TCP 连接,所以"非持久连接每传一个对象都要重来一次三次握手",代价就是每对象多 1 个 RTT。 上一章(net/42-handshake.md)"建连 1 个 RTT"这条结论,在本章被用到了极致。
原理
一、报文的四段结构
★ 请求报文与响应报文对照(必背):
text
┌─ 请求报文 ─────────────────────┐ ┌─ 响应报文 ─────────────────────┐
│ 请求行 │ │ 状态行 │
│ GET /index.html HTTP/1.1 CRLF │ │ HTTP/1.1 200 OK CRLF │
├─────────────────────────────────┤ ├─────────────────────────────────┤
│ 首部行 (每行 名称: 值 CRLF) │ │ 首部行 (每行 名称: 值 CRLF) │
│ Host: www.example.com CRLF │ │ Content-Type: text/html CRLF │
│ User-Agent: curl/8.0 CRLF │ │ Content-Length: 1024 CRLF │
│ Connection: keep-alive CRLF │ │ Last-Modified: ... CRLF │
├─────────────────────────────────┤ ├─────────────────────────────────┤
│ 空行 (单独的 CRLF) │ │ 空行 (单独的 CRLF) │
├─────────────────────────────────┤ ├─────────────────────────────────┤
│ 实体主体 (GET 时为空) │ │ 实体主体 (HTML 源码等) │
└─────────────────────────────────┘ └─────────────────────────────────┘
★ 四段: 起始行 / 首部 / 空行 / 主体; 空行是"首部结束"的唯一标志
★ 请求行的三个字段用空格分隔, 恰好两个空格 -> 三段★ 常用方法(八个,记前六个最要紧):
| 方法 | 作用 | 有无主体 | 安全/幂等 |
|---|---|---|---|
| GET | 请求读取一个资源 | 无 | 安全、幂等 |
| HEAD | 只要首部,不要主体 | 无 | 安全、幂等(常用于探测资源是否存在/是否更新) |
| POST | 向资源提交数据(表单、上传) | 有 | 不安全、不幂等 |
| PUT | 上传并替换整个资源 | 有 | 幂等 |
| DELETE | 删除资源 | 通常无 | 幂等 |
| OPTIONS | 查询该资源支持哪些方法 | 无 | 安全、幂等 |
| TRACE | 回显请求,用于诊断 | 无 | 安全(常被禁用) |
| CONNECT | 要求用隧道转发(HTTPS 代理) | 无 | — |
★ GET 与 POST 的分野(最爱考):
| 对比项 | GET | POST |
|---|---|---|
| 数据放哪 | 拼在 URL 的查询串里 | 放在请求体里 |
| 长度限制 | 受 URL 长度限制 | 理论上不限(受服务器配置限制) |
| 可见性 | 出现在 URL、浏览器历史、日志里 | 不出现 |
| 可缓存 / 可书签 | 可以 | 不可以(语义上不可缓存) |
| 幂等性 | 幂等(重复请求结果相同) | 不幂等 |
| 典型用途 | 查询、检索 | 提交、修改 |
⚠️ "GET 不能带主体"是误解:协议并不禁止 GET 带主体,但服务器普遍忽略它——考试按"GET 把参数放 URL、POST 把参数放主体"作答即可。
二、状态码:只看第一位就能分类
★ 五大类的含义(必背):
| 类 | 含义 | 谁的问题 |
|---|---|---|
| 1xx | 信息性(临时响应,继续) | — |
| 2xx | 成功 | — |
| 3xx | 重定向(资源搬家了) | 客户端要再发一次 |
| 4xx | 客户端错误(请求本身有问题) | 客户端 |
| 5xx | 服务器错误(服务器自己出问题) | 服务器 |
★ 高频状态码逐个记:
| 码 | 短语 | 含义 | 考点 |
|---|---|---|---|
| 200 | OK | 成功,响应体里有资源 | — |
| 301 | Moved Permanently | 永久重定向 | 后续请求该直接去新地址 |
| 302 | Found | 临时重定向 | 后续仍访问原地址 |
| 304 | Not Modified | 资源没变,用你本地的缓存 | ★ 响应体为空!条件 GET 的结果 |
| 400 | Bad Request | 请求报文有语法错误 | — |
| 403 | Forbidden | 服务器拒绝(权限不足) | 与 401 区分:401 是未认证,403 是认证了但不允许 |
| 404 | Not Found | 资源不存在 | 最常见 |
| 500 | Internal Server Error | 服务器内部错误 | — |
| 503 | Service Unavailable | 服务器暂时不可用(过载/维护) | — |
⚠️ 304 是最特殊的一个:它的响应里没有实体主体——因为"内容没变",服务器只回一个状态行与少量首部,让客户端直接用本地缓存。 这既省带宽又省时延,是"条件 GET"的产物。
三、连接方式与 RTT 账(本章的计算核心)
★ 三种连接方式的定义:
| 方式 | 定义 | HTTP 版本默认 |
|---|---|---|
| 非持久连接(短连接) | 每个对象一个 TCP 连接,传完就关 | HTTP/1.0 默认 |
| 持久连接(长连接) | 一个 TCP 连接里传多个对象 | HTTP/1.1 默认 |
| 流水线(pipelining) | 持久连接上"一批请求一次发出,不等回复" | HTTP/1.1 可选 |
★ 时延的三种来源(算账前先分清):
text
① 建 TCP 连接: 1 个 RTT (见 net/42-handshake.md: 第 3 个 ACK 可捎带数据)
② 一次请求-响应往返: 1 个 RTT
③ 每个对象的传输时延 = 对象大小 / 带宽 <- 题目若给了带宽就必须加上!
★ 本节的账只算 ①② 这两种"往返次数"; 若题目给了带宽, 再逐对象加 ③★ 锚点(本章定死):页面含 1 个 HTML + 3 张内嵌图片,共 4 个对象;RTT = 100 ms
| 连接方式 | 分解 | RTT 数 | 时延 |
|---|---|---|---|
| 非持久 + 串行 | 每个对象 2 RTT: | 8 | 800 ms |
| 非持久 + 2 个并行 | HTML 2 RTT + 图片分两批各 2 RTT: | 6 | 600 ms |
| 非持久 + 3 个并行 | HTML 2 RTT + 3 张图一次并行: | 4 | 400 ms |
| 持久 + 不流水线 | 建连 1 RTT + HTML 1 RTT + 3 张图各 1 RTT: | 5 | 500 ms |
| 持久 + 流水线 | 建连 1 RTT + HTML 1 RTT + 3 个请求一起发 1 RTT: | 3 | 300 ms |
★ 为什么"非持久"每个对象要 2 个 RTT:
text
第 1 个 RTT: TCP 三次握手 (SYN -> SYN+ACK -> ACK)
第 2 个 RTT: 发 HTTP 请求 -> 收到响应
★ 这就是"多花的那一个 RTT"的来历 -> 它在 408 里几乎每年都出现
★ 若用持久连接, 握手只做一次, 后续每个对象只要 1 个 RTT★ 三个结论:
- 持久连接不改变"每个对象 1 个 RTT"这件事——它省掉的是"每个对象都要重做握手"。
- 流水线才是真正压缩 RTT 的手段——它把"串行的请求-响应"变成"一次发、连续收"。
- 并行连接能替代一部分流水线的效果——但并行连接数受浏览器限制,且每条都要握手。
⚠️ 最容易错的两种情形: ① 把并行数当成"无限":题目说"浏览器开 3 个并行连接",那第 4 个对象必须等前 3 个里空出一个连接出来。② 忘记"HTML 本身也是一个对象":题目说"页面含 5 张图片",对象数是 6(5 张图 + 1 个 HTML)而不是 5——这是失分最多的一处。
四、Cookie 与缓存:给无状态协议补状态、少跑腿
★ Cookie 的四个组成部分(必背):
| 组成 | 在哪 | 作用 |
|---|---|---|
响应报文里的一行 Set-Cookie | 服务器 → 客户端 | 服务器发给客户端一个"身份证号码" |
| 被存进客户端文件的一个 cookie 文件 | 客户端本地 | 按域名保存 |
请求报文里的一行 Cookie | 客户端 → 服务器 | 后续请求自动携带(浏览器负责,应用不用管) |
| 服务器端的一张用户表 | 服务器 | 靠这个号码查"你是谁" |
★ 由此得出一条结论:HTTP 本身无状态,是"Cookie + 服务器端用户表"合起来给会话加上了状态——Cookie 里存的只是一个标识,真正的用户数据在服务器端。
★ 条件 GET 与 304(少跑腿的机制):
text
第 1 次请求: GET /logo.png
<- 200 OK + 主体 + Last-Modified: Tue, 01 Sep 2026 10:00:00 GMT
★ 客户端把资源和这个时间一起缓存起来
第 2 次请求: GET /logo.png
If-Modified-Since: Tue, 01 Sep 2026 10:00:00 GMT
★ 若服务器发现资源没变 -> 304 Not Modified (不带主体, 极省带宽)
★ 若服务器发现资源已变 -> 200 OK + 新资源
★ 这就是"条件 GET"; 另一个变体是 If-None-Match + ETag (按内容指纹判断)
★ 代理缓存 (proxy cache) 让"同一个资源被多个用户请求"时只回源一次⚠️ 缓存不能省掉"请求-响应"这一趟往返:条件 GET 仍然要发请求、收响应——它省的是"传输主体的字节数",不是 RTT。 要省 RTT,得靠持久连接与流水线。
五、HTTPS:在 HTTP 底下垫一层 TLS
★ HTTPS 的三层结构与端口:
text
HTTP 应用层 <- 报文格式不变
─────────────────
TLS/SSL <- 加密、完整性、身份认证
─────────────────
TCP 传输层 <- 端口 443 (HTTP 明文用 80)
─────────────────
★ HTTPS = HTTP over TLS; 端口 443; URL 写作 https://...★ 为什么用"对称 + 非对称混合":
| 环节 | 用什么 | 为什么 |
|---|---|---|
| 握手阶段 | 非对称加密(如 RSA / ECDHE) | 解决"密钥怎么安全地给对方"的问题——它慢,但只用于交换密钥 |
| 数据传输阶段 | 对称加密(如 AES) | 快得多,用来加密真正的大块数据 |
★ 证书链的作用:客户端内置根 CA 的公钥——服务器出示"自己的证书",证书由中间 CA 签名,中间 CA 又由根 CA 签名——逐级验签到根就确认了"服务器确实是它声称的那个域名"。 这也解决了"非对称加密如何防中间人冒充公钥"的问题。
★ HTTPS 的额外 RTT 账:
| 组合 | TCP 握手 | TLS 握手 | HTTP 请求-响应 | 总计 |
|---|---|---|---|---|
| HTTP(明文) | 1 RTT | — | 1 RTT | 2 RTT |
| HTTPS + TLS 1.2 | 1 RTT | 2 RTT | 1 RTT | 4 RTT |
| HTTPS + TLS 1.3 | 1 RTT | 1 RTT | 1 RTT | 3 RTT |
| HTTPS + TLS 1.3 会话恢复 | 1 RTT | 0 RTT | 1 RTT | 2 RTT |
★ 结论:HTTPS 比 HTTP 首次访问多 1 ~ 2 个 RTT 的握手开销——这是"加密不是免费的"最直接的体现。
六、HTTP 版本演进(对照表)
| 版本 | 默认连接 | 关键改动 | 解决了什么 |
|---|---|---|---|
| HTTP/1.0 | 非持久 | 首部 + 方法 + 状态码 | — |
| HTTP/1.1 | 持久(keep-alive) | 持久连接、流水线、Host 首部(虚拟主机)、分块传输 | 省掉重复握手 |
| HTTP/2 | 持久 + 多路复用 | 二进制分帧、单连接多路复用、首部压缩(HPACK)、服务器推送 | 解决"队头阻塞"与首部冗余 |
| HTTP/3 | 基于 QUIC(跑在 UDP 上) | 把传输层也换掉 | 彻底绕开 TCP 层的队头阻塞,握手更快 |
★ 两个易考点:Host 首部是 HTTP/1.1 新加的(一台服务器上跑多个网站,靠它区分);HTTP/2 的"多路复用"是把一条 TCP 连接切成很多"流",多个请求真正并行——而 HTTP/1.1 的流水线仍然受"响应必须按序返回"的队头阻塞限制。
示例
例 1:C 实现——请求报文解析、状态码分类与 RTT 账
参数:请求
GET /index.html HTTP/1.1;页面含 1 个 HTML + 3 张图;RTT = 100 ms。
#include <stdio.h>
#include <string.h>
/* 从 req 的 pos 处取出一行 (遇 CRLF 停止), 返回该行, 并把 pos 推进到下一行 */
static const char *nextline(const char *req, int *pos, char *out, int cap) {
int i = 0;
while (req[*pos] != '\r' && req[*pos] != '\0' && i < cap - 1)
out[i++] = req[(*pos)++];
out[i] = '\0';
if (req[*pos] == '\r') *pos += 2; /* 跳过 CRLF */
return out;
}
int main(void) {
const char *req = "GET /index.html HTTP/1.1\r\n"
"Host: www.example.com\r\n"
"User-Agent: curl/8.0\r\n"
"Connection: keep-alive\r\n"
"\r\n";
char line[128];
int pos = 0, n = 0, reqlinelen;
int i;
struct { int code; const char *cls; const char *mean; } sc[] = {
{200, "2xx", "成功"}, {301, "3xx", "永久重定向"},
{304, "3xx", "未修改 (缓存命中)"}, {404, "4xx", "资源不存在"},
{500, "5xx", "服务器内部错误"}
};
printf("(1) 请求行\n");
nextline(req, &pos, line, sizeof(line));
reqlinelen = pos; /* 请求行 + CRLF 的字节数 */
{
char *sp1 = strchr(line, ' ');
char *sp2 = strchr(sp1 + 1, ' ');
*sp1 = '\0';
*sp2 = '\0';
printf(" 方法 = %s\n", line);
printf(" 目标 = %s\n", sp1 + 1);
printf(" 版本 = %s\n", sp2 + 1);
}
printf(" ★ 请求行 = 方法 + SP + URL + SP + 版本 + CRLF\n");
printf("\n");
printf("(2) 首部字段 (每行以 CRLF 结尾; 单独的 CRLF 表示首部结束)\n");
while (1) {
nextline(req, &pos, line, sizeof(line));
if (line[0] == '\0') break; /* 空行 -> 首部结束 */
n++;
{
char *colon = strstr(line, ": ");
*colon = '\0';
printf(" %2d %-13s = %s\n", n, line, colon + 2);
}
}
printf(" ★ %d 个首部字段; 首部块 + 空行 = %d B\n", n, pos - reqlinelen);
printf(" ★ GET 请求没有 Content-Length: 请求体长度为 0\n");
printf("\n");
printf("(3) 状态码分类\n");
for (i = 0; i < 5; i++)
printf(" %d -> %s : %s\n", sc[i].code, sc[i].cls, sc[i].mean);
printf(" ★ 只看第一位: 1xx 2xx 3xx 4xx 5xx\n");
printf("\n");
printf("(4) 连接方式与 RTT (1 个 HTML + 3 张图 = 4 个对象)\n");
printf(" 非持久 + 串行 : 2 x 4 = 8 RTT\n");
printf(" 非持久 + 2 个并行 : 2 + 2 + 2 = 6 RTT\n");
printf(" 非持久 + 3 个并行 : 2 + 2 = 4 RTT\n");
printf(" 持久 + 不流水线 : 1 + 1 + 3 = 5 RTT\n");
printf(" 持久 + 流水线 : 1 + 1 + 1 = 3 RTT\n");
printf(" ★ RTT = 100 ms 时依次为 800 / 600 / 400 / 500 / 300 ms\n");
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
text
(1) 请求行
方法 = GET
目标 = /index.html
版本 = HTTP/1.1
★ 请求行 = 方法 + SP + URL + SP + 版本 + CRLF
(2) 首部字段 (每行以 CRLF 结尾; 单独的 CRLF 表示首部结束)
1 Host = www.example.com
2 User-Agent = curl/8.0
3 Connection = keep-alive
★ 3 个首部字段; 首部块 + 空行 = 71 B
★ GET 请求没有 Content-Length: 请求体长度为 0
(3) 状态码分类
200 -> 2xx : 成功
301 -> 3xx : 永久重定向
304 -> 3xx : 未修改 (缓存命中)
404 -> 4xx : 资源不存在
500 -> 5xx : 服务器内部错误
★ 只看第一位: 1xx 2xx 3xx 4xx 5xx
(4) 连接方式与 RTT (1 个 HTML + 3 张图 = 4 个对象)
非持久 + 串行 : 2 x 4 = 8 RTT
非持久 + 2 个并行 : 2 + 2 + 2 = 6 RTT
非持久 + 3 个并行 : 2 + 2 = 4 RTT
持久 + 不流水线 : 1 + 1 + 3 = 5 RTT
持久 + 流水线 : 1 + 1 + 1 = 3 RTT
★ RTT = 100 ms 时依次为 800 / 600 / 400 / 500 / 300 ms⚠️ 三点说明:
- 本机无 C 编译器:此段代码逐行人工审查,并用等价的 Python 实现实跑核对,输出逐字一致。
nextline里"遇\r就跳 2 字节"是关键:它一次性吃掉 CRLF 两个字符——所以第 2 节循环遇到"单独的空行"时,line[0]为'\0',正好作为首部结束的判据。 这也是 HTTP 报文能可靠切分的原因:首部里不允许出现裸 CRLF。- 首部块 71 B 是怎么来的:三行首部各 23 + 22 + 24 = 69 B(含各自 CRLF),再加那个空行的 2 B,共 71 B——它还没算请求行的 26 B。 所以这个请求报文本身(不含主体)是
B。
例 2:Python——三种连接方式的账、条件 GET 与 HTTPS
def pad(s, w):
"""按"显示宽度"补空格: 汉字算 2 列, 否则终端里对不齐"""
return s + ' ' * max(0, w - sum(2 if ord(c) > 0x2000 else 1 for c in s))
RTT = 100 # ms
OBJ = 4 # 1 个 HTML + 3 张图
print('=== ① 三种连接方式的 RTT 账 (对象数 = %d, RTT = %d ms) ===' % (OBJ, RTT))
ways = [('非持久 + 串行', '2 x %d' % OBJ, 2 * OBJ, '每个对象都要重新三次握手'),
('非持久 + 2 个并行', '2 + 2 + 2', 6, 'HTML 之后 3 张图分两批'),
('非持久 + 3 个并行', '2 + 2', 4, '3 张图一次并行'),
('持久 + 不流水线', '1 + 1 + 3', 5, '握手只做一次, 请求仍串行'),
('持久 + 流水线', '1 + 1 + 1', 3, '3 个请求一起发')]
print(' ' + pad('方式', 20) + pad('分解', 14) + pad('RTT', 6) + pad('时延(ms)', 10) + '说明')
for a, b, c, d in ways:
print(' ' + pad(a, 20) + pad(b, 14) + pad(str(c), 6) + pad(str(c * RTT), 10) + d)
print(' ★ 最慢 8 RTT 与最快 3 RTT, 相差 %.1f 倍' % (8 / 3))
print()
print('=== ② 为什么"非持久"每个对象要 2 个 RTT ===')
print(' 第 1 个 RTT: TCP 三次握手 (SYN -> SYN+ACK -> ACK)')
print(' 第 2 个 RTT: 发请求 -> 收响应')
print(' ★ 持久连接省掉的正是"每对象重做握手"的那 1 个 RTT')
print(' ★ 流水线省掉的是"请求-响应必须串行"的那几趟')
print()
print('=== ③ 若题目给了带宽, 还要加上传输时延 ===')
bw = 1e6 # 1 MB/s 的带宽 (约 8 Mbps)
html, img = 20000, 50000 # HTML 20 KB, 每张图 50 KB
tok = html / bw + 3 * img / bw
print(' ' + pad('连接方式', 20) + pad('往返', 10) + pad('传输(ms)', 12) + '总时延(ms)')
for a, c, d in [(w[0], w[2], w[3]) for w in ways]:
t = c * RTT + tok * 1000
print(' ' + pad(a, 20) + pad('%d RTT' % c, 10) + pad('%.1f' % (tok * 1000), 12)
+ '%.1f' % t)
print(' ★ 带宽给定时: 总时延 = 往返数 x RTT + 对象总字节 / 带宽')
print(' ★ 严格算时传输时延与往返时延会部分重叠(流水线尤甚), 408 只要求两部分相加')
print()
print('=== ④ 条件 GET 与 304 ===')
print(' ' + pad('步骤', 34) + pad('报文', 36) + '省掉了什么')
cg = [('第 1 次 GET /logo.png', '200 OK + 主体 + Last-Modified', '—'),
('客户端缓存资源与时间', '(本地保存)', '—'),
('第 2 次 GET + If-Modified-Since', '服务端比对时间', '—'),
('资源未变', '304 Not Modified (无主体)', '★ 省带宽, 不省 RTT'),
('资源已变', '200 OK + 新主体', '—')]
for a, b, c in cg:
print(' ' + pad(a, 34) + pad(b, 36) + c)
print(' ★ 另一变体: If-None-Match + ETag (按内容指纹比对)')
print()
print('=== ⑤ 状态码五大类 ===')
for cls, mean, who in [('1xx', '信息性', '—'), ('2xx', '成功', '—'),
('3xx', '重定向', '客户端要再发一次'),
('4xx', '客户端错误', '客户端'), ('5xx', '服务器错误', '服务器')]:
print(' %s %-12s 责任方: %s' % (cls, mean, who))
print(' ★ 304 是最特殊的: 无主体, 让客户端直接用缓存')
print(' ★ 401 = 未认证; 403 = 已认证但无权限')
print()
print('=== ⑥ HTTPS 的额外握手开销 ===')
print(' ' + pad('组合', 26) + pad('TCP', 8) + pad('TLS', 8) + pad('HTTP', 8) + '总计')
for a, t, l, h in [('HTTP 明文', 1, 0, 1), ('HTTPS + TLS 1.2', 1, 2, 1),
('HTTPS + TLS 1.3', 1, 1, 1), ('HTTPS + TLS 1.3 恢复', 1, 0, 1)]:
print(' ' + pad(a, 26) + pad(str(t) + ' RTT', 8) + pad(str(l) + ' RTT', 8)
+ pad(str(h) + ' RTT', 8) + '%d RTT = %d ms' % (t + l + h, (t + l + h) * RTT))
print(' ★ HTTPS 首访比 HTTP 多 1 ~ 2 个 RTT, 这就是"加密的代价"')
print()
print('=== ⑦ HTTP 版本演进 ===')
print(' ' + pad('版本', 12) + pad('默认连接', 16) + '关键改动')
for a, b, c in [('HTTP/1.0', '非持久', '首部 + 方法 + 状态码'),
('HTTP/1.1', '持久 keep-alive', '持久连接 / 流水线 / Host 首部 / 分块传输'),
('HTTP/2', '持久 + 多路复用', '二进制分帧 / 首部压缩 / 服务器推送'),
('HTTP/3', '基于 QUIC (UDP)', '换掉传输层, 绕开 TCP 队头阻塞')]:
print(' ' + pad(a, 12) + pad(b, 16) + c)
print(' ★ Host 首部是 HTTP/1.1 新加的, 用来在一台服务器上区分多个网站')
print(' ★ 流水线仍受"响应按序返回"的队头阻塞限制; HTTP/2 的多路复用才真并行')
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照(真实运行结果):
=== ① 三种连接方式的 RTT 账 (对象数 = 4, RTT = 100 ms) ===
方式 分解 RTT 时延(ms) 说明
非持久 + 串行 2 x 4 8 800 每个对象都要重新三次握手
非持久 + 2 个并行 2 + 2 + 2 6 600 HTML 之后 3 张图分两批
非持久 + 3 个并行 2 + 2 4 400 3 张图一次并行
持久 + 不流水线 1 + 1 + 3 5 500 握手只做一次, 请求仍串行
持久 + 流水线 1 + 1 + 1 3 300 3 个请求一起发
★ 最慢 8 RTT 与最快 3 RTT, 相差 2.7 倍
=== ② 为什么"非持久"每个对象要 2 个 RTT ===
第 1 个 RTT: TCP 三次握手 (SYN -> SYN+ACK -> ACK)
第 2 个 RTT: 发请求 -> 收响应
★ 持久连接省掉的正是"每对象重做握手"的那 1 个 RTT
★ 流水线省掉的是"请求-响应必须串行"的那几趟
=== ③ 若题目给了带宽, 还要加上传输时延 ===
连接方式 往返 传输(ms) 总时延(ms)
非持久 + 串行 8 RTT 170.0 970.0
非持久 + 2 个并行 6 RTT 170.0 770.0
非持久 + 3 个并行 4 RTT 170.0 570.0
持久 + 不流水线 5 RTT 170.0 670.0
持久 + 流水线 3 RTT 170.0 470.0
★ 带宽给定时: 总时延 = 往返数 x RTT + 对象总字节 / 带宽
★ 严格算时传输时延与往返时延会部分重叠(流水线尤甚), 408 只要求两部分相加
=== ④ 条件 GET 与 304 ===
步骤 报文 省掉了什么
第 1 次 GET /logo.png 200 OK + 主体 + Last-Modified —
客户端缓存资源与时间 (本地保存) —
第 2 次 GET + If-Modified-Since 服务端比对时间 —
资源未变 304 Not Modified (无主体) ★ 省带宽, 不省 RTT
资源已变 200 OK + 新主体 —
★ 另一变体: If-None-Match + ETag (按内容指纹比对)
=== ⑤ 状态码五大类 ===
1xx 信息性 责任方: —
2xx 成功 责任方: —
3xx 重定向 责任方: 客户端要再发一次
4xx 客户端错误 责任方: 客户端
5xx 服务器错误 责任方: 服务器
★ 304 是最特殊的: 无主体, 让客户端直接用缓存
★ 401 = 未认证; 403 = 已认证但无权限
=== ⑥ HTTPS 的额外握手开销 ===
组合 TCP TLS HTTP 总计
HTTP 明文 1 RTT 0 RTT 1 RTT 2 RTT = 200 ms
HTTPS + TLS 1.2 1 RTT 2 RTT 1 RTT 4 RTT = 400 ms
HTTPS + TLS 1.3 1 RTT 1 RTT 1 RTT 3 RTT = 300 ms
HTTPS + TLS 1.3 恢复 1 RTT 0 RTT 1 RTT 2 RTT = 200 ms
★ HTTPS 首访比 HTTP 多 1 ~ 2 个 RTT, 这就是"加密的代价"
=== ⑦ HTTP 版本演进 ===
版本 默认连接 关键改动
HTTP/1.0 非持久 首部 + 方法 + 状态码
HTTP/1.1 持久 keep-alive 持久连接 / 流水线 / Host 首部 / 分块传输
HTTP/2 持久 + 多路复用 二进制分帧 / 首部压缩 / 服务器推送
HTTP/3 基于 QUIC (UDP) 换掉传输层, 绕开 TCP 队头阻塞
★ Host 首部是 HTTP/1.1 新加的, 用来在一台服务器上区分多个网站
★ 流水线仍受"响应按序返回"的队头阻塞限制; HTTP/2 的多路复用才真并行五条结论:
- ★ 非持久连接的代价是"每个对象多 1 个 RTT":因为每个对象都要重新三次握手——4 个对象就是 8 个 RTT(800 ms)。
- ★ 持久连接把握手压到 1 次:4 个对象从 8 个 RTT 降到 5 个 RTT;再开流水线降到 3 个 RTT——最慢与最快相差 2.67 倍。
- ★ 并行连接能替代一部分流水线:3 个并行时只要 4 个 RTT——但并行数受浏览器限制,且非持久连接下每条连接都要握手。
- ★ 条件 GET 省的是带宽不是 RTT:304 响应没有主体,但"发请求、收响应"这一趟往返照旧要花——它省下的是那几十 KB 的图片本体,而不是时间上的一个来回。
- ★ HTTPS 首访比 HTTP 多 1 ~ 2 个 RTT:TLS 1.2 要 2 个 RTT、TLS 1.3 只要 1 个、会话恢复可到 0 个——"加密要付握手费"有了确切的数字。
考点
考点
1. 必背结论
- ★ HTTP 的两个基本特征:无状态、面向文本。
- ★ 报文四段:起始行 / 首部(每行以 CRLF 结尾)/ 空行(单独的 CRLF)/ 实体主体。
- ★ 请求行三段:方法 + SP + URL + SP + 版本;状态行三段:版本 + SP + 状态码 + SP + 短语。
- ★ 常用方法:GET / HEAD / POST / PUT / DELETE / OPTIONS(另加 TRACE / CONNECT)。
- ★ GET 与 POST 的分野:数据位置(URL vs 主体)、长度限制、可见性、可缓存、幂等性。
- ★★ 状态码五大类:1xx 信息 / 2xx 成功 / 3xx 重定向 / 4xx 客户端错误 / 5xx 服务器错误——只看第一位。
- ★ 高频码:200 成功、301 永久重定向、302 临时重定向、304 未修改(无主体)、403 禁止、404 不存在、500 服务器错误、503 暂不可用。
- ★ 401 与 403 的区别:401 未认证;403 已认证但无权限。
- ★★ 非持久连接:每个对象 2 个 RTT(1 个握手 + 1 个请求-响应);HTTP/1.0 默认。
- ★★ 持久连接:握手只做 1 次,每个对象 1 个 RTT;HTTP/1.1 默认(
Connection: keep-alive)。 - ★★ 流水线:一批请求一次性发出,不等逐个回复——把 N 个对象压到 1 个 RTT。
- ★★ 锚点(1 个 HTML + 3 张图 = 4 个对象、RTT 100 ms):非持久串行 8 RTT / 2 个并行 6 RTT / 3 个并行 4 RTT / 持久不流水线 5 RTT / 持久加流水线 3 RTT,对应 800 / 600 / 400 / 500 / 300 ms。
- ★ 若题目给了带宽:总时延 = 往返数 × RTT + 对象总字节数 ÷ 带宽——别漏了传输时延那部分。
- ★ Cookie 四要素:响应里的
Set-Cookie、客户端的 cookie 文件、请求里的Cookie首部、服务器端用户表——四者合起来才让"无状态"变成"有会话"。 - ★ 条件 GET:
If-Modified-Since(或If-None-Match+ ETag)→ 资源未变时回 304,且不带主体。 - ★ 304 省带宽不省 RTT。
- ★ Host 首部是 HTTP/1.1 新增:用于一台服务器上区分多个虚拟主机。
- ★★ HTTPS = HTTP + TLS,端口 443(HTTP 是 80);握手用非对称加密交换密钥、数据用对称加密传输;证书链解决身份认证。
- ★★ HTTPS 的额外 RTT:TLS 1.2 加 2 个 RTT、TLS 1.3 加 1 个、会话恢复加 0 个。
- ★ 版本演进:1.0 非持久 → 1.1 持久与流水线 → 2 二进制分帧与多路复用 → 3 基于 QUIC(UDP)。
2. 高频陷阱
- 把 Cookie 说成"服务器端的状态":要分清。Cookie 存在客户端;真正的用户数据在服务器端的用户表里——Cookie 里只是一个标识。
- 认为"HTTP 有状态":错。HTTP 本身无状态——是 Cookie + 服务器端表"人为"加上了状态。
- 把 302 与 301 搞混:要分清。301 永久(该改书签)、302 临时(仍用原地址)。
- 忘记 304 的响应没有主体:错,这是 304 最本质的特点。
- 把 404 与 403 混为一谈:要分清。404 是"没有这个资源";403 是"有,但你不许看"。
- 把 500 与 503 混为一谈:500 是"服务器内部出错";503 是"服务器暂时不可用(过载/维护)"。
- 把"非持久连接每个对象 2 个 RTT"记成"1 个":错,漏掉了 TCP 握手那 1 个 RTT——这是本章最核心的一句。
- 忘记把 HTML 本身算成一个对象:错。"页面含 5 张图"是 6 个对象。
- 把"持久连接"当成"流水线":错。持久只省握手;流水线才省"请求-响应串行"那几趟。 两者可以叠加。
- 认为"流水线完全解决了队头阻塞":不准确。HTTP/1.1 的流水线仍要求响应按序返回;真正的多路复用是 HTTP/2 的二进制分帧。
- 把 HTTPS 说成"另一种协议":错。它是 HTTP 报文 + TLS 加密通道,报文格式完全不变。
- 认为"HTTPS 全程用非对称加密":错。非对称只用于握手交换密钥;数据用对称加密。 否则性能不可接受。
- 把 HTTPS 端口记成 8080 或 8443:错。默认 443(HTTP 是 80)。
- 认为"HTTPS 一定比 HTTP 慢很多":不准确。首次访问多 1 ~ 2 个 RTT 的握手;有了会话恢复或 TLS 1.3,差距就小了。
- 把"条件 GET 能省时延"当成结论:错。它省的是带宽(不传主体),往返次数不变。
- 认为 GET 请求一定能被缓存且 POST 一定不能:看语义。协议允许缓存 GET 的响应;POST 的响应默认不缓存。
3. 解题模板("HTTP 时延题")
① 数清对象:
对象数 = 1 个 HTML + 内嵌对象(图片/CSS/JS) 个数
★ 最容易漏掉 HTML 本身
② 判断连接方式:
题目说"非持久"或 HTTP/1.0 -> 每对象 2 RTT
题目说"持久 (keep-alive)" -> 握手 1 RTT + 每对象 1 RTT
题目说"流水线" -> 握手 1 RTT + HTML 1 RTT + 其余请求合并 1 RTT
③ 处理并行数 k:
非持久: 先 2 RTT 取 HTML, 剩下 N 个对象按 ceil(N/k) 批, 每批 2 RTT
持久 : 握手 1 RTT + HTML 1 RTT + 剩下 N 个对象按 ceil(N/k) 批, 每批 1 RTT
④ 加上传输时延 (若给了带宽):
总时延 = 往返数 x RTT + 对象总字节数 / 带宽
⑤ 若题目提到 HTTPS:
在上面的基础上加 TLS 握手: 1.2 -> +2 RTT; 1.3 -> +1 RTT; 恢复 -> +0 RTT4. 与相邻章节的接口
net/42-handshake.md(连接管理):"建连 1 个 RTT"是本章所有账的基石;非持久连接多花的那个 RTT,就是三次握手的成本。net/41-tcp.md(TCP 报文段):HTTP 报文是被 TCP 当字节流传输的——所以 HTTP 首部里不允许出现裸 CRLF(否则边界会乱),这与 TCP 的字节流语义直接相关。net/50-dns.md(DNS):"访问一个页面"的完整时延 = DNS 解析(可能 2 ~ 8 个报文)+ TCP 握手 + HTTP 请求-响应——DNS 用 UDP 省下的 1 个 RTT 与本章的账可以叠起来算。net/52-others.md(应用层协议概览):本章的"请求-响应"模式与下一章的"推送(SMTP)/拉取(POP3)"模式正好形成对照。soft/31-crypto.md(密码学基础):HTTPS 的"对称 + 非对称混合加密""证书链""MAC 与签名的分工"都在那一章展开——本章只从"多几个 RTT"的角度切入。os/30-filesystem.md与os/32-io.md:浏览器的缓存写在本地磁盘上(缓存目录 + 索引);"304 用本地缓存"这句话在操作系统层面的含义就是"读本地文件"。
小结
- ★ HTTP 无状态、面向文本;报文四段:起始行 / 首部 / 空行 / 主体。
- ★ 状态码五大类:1xx / 2xx / 3xx / 4xx / 5xx;常考 200、301、302、304(无主体)、403、404、500。
- ★★ 非持久连接每对象 2 个 RTT(握手 1 + 请求响应 1);持久连接握手只 1 次,每对象 1 个 RTT;流水线把一批请求压成 1 个 RTT。
- ★★ 锚点(4 个对象、RTT 100 ms):8 / 6 / 4 / 5 / 3 个 RTT,即 800 / 600 / 400 / 500 / 300 ms。
- ★ 有带宽时加传输时延:总时延 = 往返数 × RTT + 对象总字节 ÷ 带宽。
- ★ Cookie 四要素让无状态协议有了会话;条件 GET 的 304 省带宽但不省 RTT。
- ★★ HTTPS = HTTP + TLS(端口 443):握手非对称、数据对称、证书验身份;比 HTTP 多 1 ~ 2 个 RTT。
- ★ 版本演进:1.0 非持久 → 1.1 持久 + Host 首部 → 2 多路复用 → 3 基于 QUIC。
上一篇:DNS 域名解析 | 下一篇:FTP、SMTP、POP3 等协议概览
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。