以今天部署的那台服务器为例

一次视频下载的 TCP 与 HTTP

家里人在手机上点「下载」,到视频落到本地,中间发生了什么。把 HTTP 和 TCP 拆开看:TCP 负责"把字节可靠地送到对面",HTTP 负责"说清楚要什么、回什么"。HTTP 跑在 TCP 里面,两者分工明确。

先看谁包着谁

一个 HTTP 报文不是直接上网的。它被一层层装进去,每层只管自己那件事;对面收到后再一层层拆开。

网卡 / 交换机
以太网帧
本地这一跳:从你的手机到路由器、从机房交换机到服务器网卡。
路由器(沿途每一台)
IP 包
跨网络找到 192.144.191.25。只管"往哪投递",不保证送达、不保证顺序。
操作系统内核(两端)
TCP 段
按端口找到进程(:80 是 nginx);给每个字节编号、丢了重传、乱序重排、控制发送速度。应用看到的是一条连续可靠的字节流
nginx / 浏览器
HTTP 报文
一段按格式写的文本:请求行 + 头部 + 空行 + 正文。"我要 /dl/xxx.mp4 的前 1024 字节","给你,206"。
真正想传的东西
视频字节
165 MB 的 mp4。HTTP 头里的 Content-Length 告诉对方这次读多少字节算完。
外层到内层:以太网 → IP → TCP → HTTP → 数据。上一层只把下一层当成"一坨字节",不理解它的含义——所以 TCP 不知道自己正在传 HTTP,HTTP 也不知道底下丢过几次包。

时序:从点击到关闭连接

点「下一步」逐条看,或直接点图中任意一行。每一步都标了它属于哪一层、原始报文长什么样、以及人话翻译。报文内容取自今天对 192.144.191.25 的真实 curl 结果。

左侧是家人的手机(客户端),右侧是服务器上的 nginx。绿色为 TCP 层报文,橙色为 HTTP 层报文,虚线为说明。
TCP:内核之间的对话HTTP:浏览器与 nginx 的对话说明

最容易混的五件事

"HTTP 连接"其实不存在

连接是 TCP 的。HTTP 只是在这条连接里来回传文本。所谓 keep-alive,就是"这条 TCP 连接先别关,我还要发下一个请求"。

一个请求 ≠ 一个包

一个 HTTP 请求可能跨多个 TCP 段;165 MB 的响应被切成约 11 万个段逐个送达、逐个确认。HTTP 层对此一无所知,它只看到"字节源源不断地来"。

慢,多半不是 nginx 慢

今天测到约 0.7 MB/s,是服务器出口带宽和 TCP 窗口推进速度决定的。nginx 开了 sendfile,只是把文件交给内核发送,自己几乎不干活。

端口:服务器固定,客户端随机

:80 是 nginx 守着的门。手机那头每次连接都是一个随机高位端口(如 51342)。四元组(两端 IP + 两端端口)唯一标识一条连接。

HTTPS 只是多一层

三次握手之后、HTTP 之前,再做一次 TLS 握手(交换证书、协商密钥)。之后跑的 HTTP 报文一模一样,只是加了密。

断点续传靠 Range

下载中断后,客户端带 Range: bytes=52428800- 再发一次请求,服务器回 206 只给后半段。今天 nginx 对静态文件默认就支持。

名词速查

属于它是什么
seq / ackTCP序号:给字节流里每个字节编的号。ack=N 表示"N 之前的我都收到了,下一个给我 N"。有序、去重、重传全靠它。
SYN / FIN / ACKTCP段头里的标志位。SYN=我要建连接,FIN=我这边说完了,ACK=我在确认你的序号。
MSSTCP一个段最多放多少字节数据,通常 1460。握手时双方互报。
窗口 (win)TCP"不等确认我最多先发这么多"。窗口大、RTT 小,吞吐就高。这是下载速度的真正瓶颈所在。
RTTTCP一来一回的时间。三次握手就是 1 个 RTT 的代价,keep-alive 省的就是它。
TIME_WAITTCP主动关闭的一方在挥手后要等 2 倍报文最大生存时间,确保最后的 ACK 送达、旧包在网上消散。
请求行 / 状态行HTTPGET /path HTTP/1.1HTTP/1.1 206 Partial Content。报文第一行,决定"做什么"和"结果如何"。
Content-LengthHTTP正文有多少字节。对方读够这个数就知道这条响应结束了——这是同一连接里多个响应不串的前提。
Range / 206HTTP请求"只要第 a 到 b 字节",响应 206 并用 Content-Range 说明给的是哪一段、总共多大。
Keep-AliveHTTPHTTP/1.1 默认行为:响应完不关 TCP 连接。nginx 的 keepalive_timeout 65 = 空闲 65 秒后才关。
Content-DispositionHTTPattachment 告诉浏览器"别预览,存成文件"。今天的 /dl/ 路径就靠它。

自己动手看一遍

不要只看图。这两条命令分别从 HTTP 层和 TCP 层把今天这台服务器的真实流量打出来,对照上面的时序图看一遍,比任何教程都管用。

HTTP 在本机看报文

打印请求头(>)和响应头(<),只取前 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'

TCP 在服务器上抓包

SSH 到服务器先跑它,再在本机跑左边的 curl,就能看到 [S] [S.] [.] 握手、[P.] 带数据、[F.] 挥手。

tcpdump -i any -nn 'tcp port 80' -c 40