家里人在手机上点「下载」,到视频落到本地,中间发生了什么。把 HTTP 和 TCP 拆开看:TCP 负责"把字节可靠地送到对面",HTTP 负责"说清楚要什么、回什么"。HTTP 跑在 TCP 里面,两者分工明确。
一个 HTTP 报文不是直接上网的。它被一层层装进去,每层只管自己那件事;对面收到后再一层层拆开。
192.144.191.25。只管"往哪投递",不保证送达、不保证顺序。:80 是 nginx);给每个字节编号、丢了重传、乱序重排、控制发送速度。应用看到的是一条连续可靠的字节流。/dl/xxx.mp4 的前 1024 字节","给你,206"。Content-Length 告诉对方这次读多少字节算完。点「下一步」逐条看,或直接点图中任意一行。每一步都标了它属于哪一层、原始报文长什么样、以及人话翻译。报文内容取自今天对 192.144.191.25 的真实 curl 结果。
连接是 TCP 的。HTTP 只是在这条连接里来回传文本。所谓 keep-alive,就是"这条 TCP 连接先别关,我还要发下一个请求"。
一个 HTTP 请求可能跨多个 TCP 段;165 MB 的响应被切成约 11 万个段逐个送达、逐个确认。HTTP 层对此一无所知,它只看到"字节源源不断地来"。
今天测到约 0.7 MB/s,是服务器出口带宽和 TCP 窗口推进速度决定的。nginx 开了 sendfile,只是把文件交给内核发送,自己几乎不干活。
:80 是 nginx 守着的门。手机那头每次连接都是一个随机高位端口(如 51342)。四元组(两端 IP + 两端端口)唯一标识一条连接。
三次握手之后、HTTP 之前,再做一次 TLS 握手(交换证书、协商密钥)。之后跑的 HTTP 报文一模一样,只是加了密。
下载中断后,客户端带 Range: bytes=52428800- 再发一次请求,服务器回 206 只给后半段。今天 nginx 对静态文件默认就支持。
| 词 | 属于 | 它是什么 |
|---|---|---|
| seq / ack | TCP | 序号:给字节流里每个字节编的号。ack=N 表示"N 之前的我都收到了,下一个给我 N"。有序、去重、重传全靠它。 |
| SYN / FIN / ACK | TCP | 段头里的标志位。SYN=我要建连接,FIN=我这边说完了,ACK=我在确认你的序号。 |
| MSS | TCP | 一个段最多放多少字节数据,通常 1460。握手时双方互报。 |
| 窗口 (win) | TCP | "不等确认我最多先发这么多"。窗口大、RTT 小,吞吐就高。这是下载速度的真正瓶颈所在。 |
| RTT | TCP | 一来一回的时间。三次握手就是 1 个 RTT 的代价,keep-alive 省的就是它。 |
| TIME_WAIT | TCP | 主动关闭的一方在挥手后要等 2 倍报文最大生存时间,确保最后的 ACK 送达、旧包在网上消散。 |
| 请求行 / 状态行 | HTTP | GET /path HTTP/1.1 与 HTTP/1.1 206 Partial Content。报文第一行,决定"做什么"和"结果如何"。 |
| Content-Length | HTTP | 正文有多少字节。对方读够这个数就知道这条响应结束了——这是同一连接里多个响应不串的前提。 |
| Range / 206 | HTTP | 请求"只要第 a 到 b 字节",响应 206 并用 Content-Range 说明给的是哪一段、总共多大。 |
| Keep-Alive | HTTP | HTTP/1.1 默认行为:响应完不关 TCP 连接。nginx 的 keepalive_timeout 65 = 空闲 65 秒后才关。 |
| Content-Disposition | HTTP | attachment 告诉浏览器"别预览,存成文件"。今天的 /dl/ 路径就靠它。 |
不要只看图。这两条命令分别从 HTTP 层和 TCP 层把今天这台服务器的真实流量打出来,对照上面的时序图看一遍,比任何教程都管用。
打印请求头(>)和响应头(<),只取前 1024 字节免得刷屏。
curl -v -r 0-1023 -o /dev/null \
'http://192.144.191.25/videos/%E5%9F%B9%E5%85%BB6~18%E5%B2%81%E5%A4%A9%E6%89%8D%E5%B0%91%E5%B9%B4%EF%BC%81.mp4'SSH 到服务器先跑它,再在本机跑左边的 curl,就能看到 [S] [S.] [.] 握手、[P.] 带数据、[F.] 挥手。
tcpdump -i any -nn 'tcp port 80' -c 40