一次 HTTP 请求的完整旅程:从 TCP 握手到数据传输
你每天都在用 HTTP,但你知道按下回车之后,网络里到底发生了什么吗?
这篇文章从最底层开始,把一次完整的 HTTP 请求拆开来看——每一个包长什么样,每一个字节代表什么意思。
目录
- 全局视角:五个阶段
- 第零步:DNS 解析——先找到地址
- 数据包的结构:洋葱模型
- TCP 三次握手:建立连接
- TCP 标志位(Flags)完全解析
- HTTP 请求包:发出去的是什么
- HTTP 响应包:收到的是什么
- TCP 四次挥手:关闭连接
- IP Header 字段详解
- TCP Header 字段详解
- 关键概念速查表
1. 全局视角:五个阶段
你在浏览器输入 http://tpforever.com 按下回车,到页面显示出来,底层经历了五个阶段:
你的机器 (192.168.9.55) 服务器 (146.190.62.39)
│ │
│ ① DNS 查询 │
│ "tpforever.com 的 IP 是多少?" │
│─────────────────────────────────────────►│ DNS服务器
│◄─────────────────────────────────────────│ "146.190.62.39"
│ │
│ ② TCP 三次握手(建立连接) │
│──── [SYN] ────────────────────────────►│
│◄─── [SYN, ACK] ─────────────────────────│
│──── [ACK] ────────────────────────────►│
│ │
│ ③ HTTP 请求 │
│──── GET / HTTP/1.1 ─────────────────────►│
│ │
│ ④ HTTP 响应(可能分多个包) │
│◄─── HTTP/1.1 200 OK + HTML ──────────────│
│ │
│ ⑤ TCP 四次挥手(关闭连接) │
│◄─── [FIN, ACK] ─────────────────────────│
│──── [ACK] ────────────────────────────►│整个过程中,数据在 4 个协议层之间流动,每层只负责自己的部分:
┌──────────────────────────────────────────────────────┐
│ 应用层 (HTTP) │
│ GET / HTTP/1.1\r\nHost: tpforever.com\r\n\r\n │
├──────────────────────────────────────────────────────┤
│ 传输层 (TCP) │
│ src_port=54775 dst_port=80 seq=1 flags=[PSH,ACK] │
├──────────────────────────────────────────────────────┤
│ 网络层 (IP) │
│ src=192.168.9.55 dst=146.190.62.39 TTL=128 │
├──────────────────────────────────────────────────────┤
│ 链路层 (Ethernet) │
│ src_mac=aa:bb:cc dst_mac=dd:ee:ff │
└──────────────────────────────────────────────────────┘类比理解:就像寄快递——你写的信(HTTP)装进信封(TCP),信封上写地址(IP),快递员按路线送(Ethernet)。每一层只看自己那部分,不管里面装的是什么。
2. 第零步:DNS 解析——先找到地址
TCP 连接需要 IP 地址,不是域名。tpforever.com 是给人看的,146.190.62.39 才是机器用的。在第一个 SYN 包发出之前,必须先完成这个翻译。
你的程序调用 gethostbyname("tpforever.com")
│
▼
① 先查本地缓存(上次查过就直接用)
│ 没有缓存
▼
② 问递归解析器(你的路由器 / 运营商 / 8.8.8.8)
│ 它也不知道
▼
③ 根域名服务器:".com 的服务器在这里"
│
▼
④ .com 服务器:"tpforever.com 的权威服务器在这里"
│
▼
⑤ 权威服务器:"tpforever.com = 146.190.62.39,缓存 3600 秒"
│
▼
返回 IP,开始 TCP 连接DNS 查询包长什么样(UDP 端口 53):
原始字节:
12 34 01 00 00 01 00 00 00 00 00 00 09 74 70 66
6f 72 65 76 65 72 03 63 6f 6d 00 00 01 00 01
逐字节解析:
12 34 Transaction ID = 0x1234(用来匹配请求和响应)
01 00 Flags = 0x0100(QR=0 表示这是查询,RD=1 表示希望递归解析)
00 01 QDCOUNT = 1(1 个问题)
00 00 ANCOUNT = 0(0 个答案,这是查询包)
00 00 NSCOUNT = 0
00 00 ARCOUNT = 0
09 域名第一段长度 = 9
74 70 66 6f 72 65 76 65 72 "tpforever"(9个字节)
03 域名第二段长度 = 3
63 6f 6d "com"(3个字节)
00 域名结束符
00 01 QTYPE = 1(A记录,查 IPv4 地址)
00 01 QCLASS = 1(Internet)三个关键点:
- DNS 用 UDP,不用 TCP——查询包很小,不需要建连接的开销
- 每条记录有 TTL(缓存时间)。TTL 是"改了 DNS 要等生效"的根本原因
- 域名编码规则:每段前加长度字节,
tpforever.com→\x09tpforever\x03com\x00
3. 数据包的结构:洋葱模型
在深入每个阶段之前,先理解一个核心概念:每个网络包都是一层套一层的结构,就像洋葱。
一个完整的 HTTP 数据包(在网线上传输的字节):
┌────────────────────────────────────────────────────────────────┐
│ Ethernet Header (14 bytes) │
│ dst_mac(6) + src_mac(6) + type(2) │
├────────────────────────────────────────────────────────────────┤
│ IP Header (20 bytes) │
│ version + TTL + protocol + src_ip(4) + dst_ip(4) + ... │
├────────────────────────────────────────────────────────────────┤
│ TCP Header (20~60 bytes) │
│ src_port(2) + dst_port(2) + seq(4) + ack(4) + flags + ... │
├────────────────────────────────────────────────────────────────┤
│ HTTP Data (payload) │
│ GET / HTTP/1.1\r\nHost: ...\r\n\r\n │
└────────────────────────────────────────────────────────────────┘
发送时:从上往下,每层加自己的 header(封装)
接收时:从下往上,每层剥掉自己的 header(解封装)每一层只看自己的 header,对其他层的内容完全不透明。IP 层不知道你在用 HTTP 还是 FTP,它只看 IP header 里的目标地址。
4. TCP 三次握手:建立连接
为什么需要握手?
TCP 保证数据可靠传输——不丢、不乱序、不重复。实现这个保证的基础是序号机制:每个字节都有编号,丢了就重传。
握手的目的就是:双方交换各自的初始序号(ISN),同时确认对方能收能发。
握手过程
客户端 (192.168.9.55:54775) 服务器 (146.190.62.39:80)
│ │
│ 第一步:SYN │
│ "我想连接,我的初始序号是 X" │
│ Flags=[SYN] Seq=0 │
│──────────────────────────────────►│
│ │
│ 第二步:SYN-ACK │
│ "收到,我的序号是 Y,期待你发 X+1" │
│ Flags=[SYN,ACK] Seq=0 Ack=1 │
│◄──────────────────────────────────│
│ │
│ 第三步:ACK │
│ "收到,期待你发 Y+1" │
│ Flags=[ACK] Ack=1 │
│──────────────────────────────────►│
│ │
│ ══════ 连接建立,开始传数据 ══════ │实例:三个握手包的完整字节
第一步:SYN 包(客户端 → 服务器)
原始字节(52 字节):
0000 45 00 00 34 51 7f 40 00 80 06 0e 1f c0 a8 09 37
0010 92 be 3e 27 d5 f7 00 50 85 12 8e c4 00 00 00 00
0020 80 02 ff ff ea 90 00 00 02 04 05 50 01 03 03 08
0030 01 01 04 02偏移对照表(SYN 包,52 字节)
左侧
0000 / 0010 / 0020 / 0030是十六进制偏移量,是抓包工具加的"行号",不是包的内容。
每行显示 16 个字节,所以偏移每次加0x10(十进制 16)。
| 偏移(hex) | 偏移(dec) | 字节内容 | 所属层 | 字段 | 解释 |
|---|---|---|---|---|---|
0x00 |
0 | 45 |
IP | Version + IHL | IPv4,头部长度 = 5×4 = 20 字节 |
0x01 |
1 | 00 |
IP | DSCP/ECN | 普通流量,无 QoS 标记 |
0x02–03 |
2–3 | 00 34 |
IP | Total Length | 整包 = 52 字节 |
0x04–05 |
4–5 | 51 7f |
IP | Identification | 分片标识符 |
0x06–07 |
6–7 | 40 00 |
IP | Flags + Frag Offset | DF=1(不分片),偏移=0 |
0x08 |
8 | 80 |
IP | TTL | 128 → Windows 系统 |
0x09 |
9 | 06 |
IP | Protocol | 6 = TCP |
0x0A–0B |
10–11 | 0e 1f |
IP | Header Checksum | IP 头校验和 |
0x0C–0F |
12–15 | c0 a8 09 37 |
IP | Source IP | 192.168.9.55(客户端) |
0x10–13 |
16–19 | 92 be 3e 27 |
IP | Destination IP | 146.190.62.39(服务器) |
0x14–15 |
20–21 | d5 f7 |
TCP | Source Port | 54775(临时端口) |
0x16–17 |
22–23 | 00 50 |
TCP | Destination Port | 80(HTTP) |
0x18–1B |
24–27 | 85 12 8e c4 |
TCP | Sequence Number | 客户端 ISN |
0x1C–1F |
28–31 | 00 00 00 00 |
TCP | Acknowledgment | 0(SYN 包无确认) |
0x20 |
32 | 80 |
TCP | Data Offset | 8×4 = 32 字节(含 Options) |
0x21 |
33 | 02 |
TCP | Flags | 0000 0010 → SYN=1 |
0x22–23 |
34–35 | ff ff |
TCP | Window Size | 65535 |
0x24–25 |
36–37 | ea 90 |
TCP | Checksum | TCP 校验和 |
0x26–27 |
38–39 | 00 00 |
TCP | Urgent Pointer | 0(URG=0,无效) |
0x28–2B |
40–43 | 02 04 05 50 |
TCP Options | MSS | 最大数据段 = 1360 字节 |
0x2C |
44 | 01 |
TCP Options | NOP | 填充对齐 |
0x2D–2F |
45–47 | 03 03 08 |
TCP Options | Window Scale | 实际窗口 = 65535 × 2⁸ |
0x30 |
48 | 01 |
TCP Options | NOP | 填充对齐 |
0x31 |
49 | 01 |
TCP Options | NOP | 填充对齐 |
0x32–33 |
50–51 | 04 02 |
TCP Options | SACK Permitted | 支持选择性确认 |
字节布局示意:
偏移 0x00 0x0F
├── IP Header(20字节)──┤
偏移 0x14 0x27
├── TCP Header(20字节)─┤
偏移 0x28 0x33
├── TCP Options(12字节)┤
(无 Payload,SYN 包不携带数据)━━━ IP Header(偏移 0x00,共 20 字节)━━━
45 Version=4, IHL=5(头部长度 = 5×4 = 20字节)
00 服务类型 = 0(普通流量)
00 34 总长度 = 52 字节
51 7f 标识符(分片用)
40 00 Flags=Don't Fragment,分片偏移=0
80 TTL = 128(Windows 系统,初始值128)
06 Protocol = 6 → 这是 TCP 包
0e 1f IP 校验和
c0 a8 09 37 源 IP = 192.168.9.55(客户端)
92 be 3e 27 目标 IP = 146.190.62.39(服务器)
━━━ TCP Header(偏移 0x14,共 32 字节含选项)━━━
d5 f7 源端口 = 54775(客户端随机分配的临时端口)
00 50 目标端口 = 80(HTTP 标准端口)
85 12 8e c4 序号 = 0x85128EC4(客户端 ISN,Wireshark 显示相对值 0)
00 00 00 00 确认号 = 0(SYN 包还没有确认任何东西)
80 数据偏移 = 8(头部长度 = 8×4 = 32字节)
02 Flags = 0x02 = 0000 0010
┌─────┬─────┬─────┬─────┬─────┬─────┐
│ URG │ ACK │ PSH │ RST │ SYN │ FIN │
│ 0 │ 0 │ 0 │ 0 │ 1 │ 0 │ ← 只有 SYN=1
└─────┴─────┴─────┴─────┴─────┴─────┘
ff ff 窗口大小 = 65535(客户端接收缓冲区大小)
ea 90 TCP 校验和
00 00 紧急指针 = 0(URG=0,此字段无效)
TCP Options(12字节):
02 04 05 50 MSS=1360(客户端能接受的最大数据段)
01 NOP(填充)
03 03 08 Window Scale=8(实际窗口 = 65535 × 2^8 = 16,776,960)
01 NOP(填充)
01 NOP(填充)
04 02 SACK Permitted(支持选择性确认)
━━━ Payload ━━━
(无,SYN 包不携带数据)第二步:SYN-ACK 包(服务器 → 客户端)
原始字节(52 字节):
0000 45 00 00 34 00 00 40 00 2e 06 b1 ff 92 be 3e 27
0010 c0 a8 09 37 00 50 d5 f7 13 21 09 c5 85 12 8e c5
0020 80 12 fa f0 d2 77 00 00 02 04 05 82 01 01 04 02
0030 01 03 03 07
━━━ IP Header ━━━
45 Version=4, IHL=5
00 34 总长度 = 52 字节
2e TTL = 46(Linux 服务器,初始64,经过约18跳)
06 Protocol = TCP
92 be 3e 27 源 IP = 146.190.62.39(服务器)
c0 a8 09 37 目标 IP = 192.168.9.55(客户端)
━━━ TCP Header ━━━
00 50 源端口 = 80
d5 f7 目标端口 = 54775
13 21 09 c5 序号 = 0x132109C5(服务器 ISN,Wireshark 显示相对值 0)
85 12 8e c5 确认号 = 0x85128EC5(= 客户端 ISN + 1,确认收到 SYN)
80 数据偏移 = 8(头部32字节)
12 Flags = 0x12 = 0001 0010
┌─────┬─────┬─────┬─────┬─────┬─────┐
│ URG │ ACK │ PSH │ RST │ SYN │ FIN │
│ 0 │ 1 │ 0 │ 0 │ 1 │ 0 │ ← SYN=1, ACK=1
└─────┴─────┴─────┴─────┴─────┴─────┘
fa f0 窗口大小 = 64240
d2 77 TCP 校验和
TCP Options:
02 04 05 82 MSS=1410(服务器能接受的最大数据段)
01 01 NOP × 2
04 02 SACK Permitted
01 NOP
03 03 07 Window Scale=7(实际窗口 = 64240 × 2^7 = 8,222,720)注意 TTL 的差异:
- 客户端 TTL=128 → Windows 系统(Windows 初始 TTL=128)
- 服务器 TTL=46 → Linux 系统(Linux 初始 TTL=64,64-46=18,说明经过了约 18 个路由器)
第三步:ACK 包(客户端 → 服务器)
原始字节(40 字节,最小的 TCP 包):
0000 45 00 00 28 51 80 40 00 80 06 0e 30 c0 a8 09 37
0010 92 be 3e 27 d5 f7 00 50 85 12 8e c5 13 21 09 c6
0020 50 10 fd 20 xx xx 00 00
━━━ TCP Header ━━━
d5 f7 源端口 = 54775
00 50 目标端口 = 80
85 12 8e c5 序号 = 客户端 ISN+1(相对值 1)
13 21 09 c6 确认号 = 服务器 ISN+1(相对值 1)
50 数据偏移 = 5(头部 = 5×4 = 20字节,无选项)
10 Flags = 0x10 = 0001 0000
┌─────┬─────┬─────┬─────┬─────┬─────┐
│ URG │ ACK │ PSH │ RST │ SYN │ FIN │
│ 0 │ 1 │ 0 │ 0 │ 0 │ 0 │ ← 只有 ACK=1
└─────┴─────┴─────┴─────┴─────┴─────┘
fd 20 窗口大小 = 64800
━━━ Payload ━━━
(无,纯确认包,Len=0)为什么是三次,不是两次?
两次握手的问题:
客户端 ──SYN──► 服务器 ✓ 服务器知道"客户端能发"
客户端 ◄─SYN-ACK── 服务器 但如果这个包丢了?
服务器以为连接建立了,分配了资源
客户端却不知道,不会发数据
→ 服务器白白占用资源,无法释放
第三次 ACK 的作用:
服务器收到 ACK 才确认"客户端也能收到我的包"
才真正分配连接资源
→ 防止半开连接浪费服务器资源序号(Seq)和确认号(Ack)的关系
这是 TCP 可靠性的核心,理解了这个就理解了 TCP:
发送方给每个字节编号:
发送 "Hello"(5字节),从 Seq=100 开始
H → seq=100
e → seq=101
l → seq=102
l → seq=103
o → seq=104
接收方用 Ack 告诉发送方"我期待下一个字节的编号":
收到 seq=100~104 → 回复 Ack=105("105之前的我都收到了")
如果 seq=102 的包丢了:
接收方一直回复 Ack=102("我在等102")
发送方收到 3 个重复 Ack=102 → 触发重传
重传 seq=102 → 接收方收到后一次性 Ack=1055. TCP 标志位(Flags)完全解析
TCP header 里有 6 个标志位,每个占 1 bit,用来表示这个包的"类型":
Flags 字节(1 byte = 8 bits,低 6 位有效):
bit 7 bit 6 bit 5 bit 4 bit 3 bit 2 bit 1 bit 0
┌──────┬──────┬──────┬──────┬──────┬──────┬──────┬──────┐
│ - │ - │ URG │ ACK │ PSH │ RST │ SYN │ FIN │
└──────┴──────┴──────┴──────┴──────┴──────┴──────┴──────┘
常见值:
0x02 = 0000 0010 → [SYN] 握手第一步
0x12 = 0001 0010 → [SYN, ACK] 握手第二步
0x10 = 0001 0000 → [ACK] 纯确认
0x18 = 0001 1000 → [PSH, ACK] 携带数据
0x11 = 0001 0001 → [FIN, ACK] 关闭连接
0x04 = 0000 0100 → [RST] 强制重置SYN — 发起连接
- 只在握手阶段出现,整个连接只用两次(SYN 包和 SYN-ACK 包)
- SYN=1 的包会消耗 1 个序号,即使没有数据(Len=0)
- 携带 MSS、Window Scale、SACK 等协商参数
SYN 包的作用:
"我想和你建立连接,我的初始序号是 X,
我最大能接受 1360 字节的数据段,
我支持选择性确认..."ACK — 确认收到
- 握手第二步之后,几乎每个包都带 ACK
Ack字段的值 = 期待对方下一个包的序号- 累积确认:一个 ACK 可以确认之前所有的数据
Ack=109 的含义:
"108 及之前的字节我都收到了,期待你发 seq=109"PSH — 立刻推送
- 告诉接收方的操作系统:"不要等缓冲区满,现在就把数据交给应用程序"
- HTTP 请求包和响应包通常都带 PSH
- 减少延迟,适合交互式通信
没有 PSH:OS 可能攒够一批数据再交给应用,增加延迟
有 PSH:OS 立刻把数据交给应用处理FIN — 结束发送
- 主动关闭方发送,表示"我这边的数据发完了"
- FIN 也消耗 1 个序号(和 SYN 一样)
- FIN 只关闭一个方向,TCP 是全双工,另一方可以继续发
RST — 强制重置
- 立刻终止连接,不走正常的四次挥手
- 常见场景:连接到没有监听的端口、防火墙强制断开
- 和 FIN 的区别:FIN 是"我发完了,等你";RST 是"连接立刻终止"
URG — 紧急数据
- 极少使用,了解即可
- 表示数据中有需要优先处理的紧急内容
常见 Flag 组合速查
| Flags 值 | 组合 | 出现场景 |
|---|---|---|
0x02 |
[SYN] |
握手第一步,客户端发起 |
0x12 |
[SYN, ACK] |
握手第二步,服务器响应 |
0x10 |
[ACK] |
纯确认,无数据 |
0x18 |
[PSH, ACK] |
携带数据(HTTP 请求/响应) |
0x11 |
[FIN, ACK] |
关闭连接 |
0x04 |
[RST] |
异常终止 |
6. HTTP 请求包:发出去的是什么
握手完成后,客户端发出 HTTP 请求。这个包有三层内容:IP Header + TCP Header + HTTP 数据。
完整的 HTTP 请求包字节
原始字节(147 字节):
0000 45 00 00 93 51 81 40 00 80 06 0e 1f c0 a8 09 37
0010 92 be 3e 27 d5 f7 00 50 85 12 8e c5 13 21 09 c6
0020 50 18 ff 85 00 00 47 45 54 20 2f 20 48 54 54 50
0030 2f 31 2e 31 0d 0a 48 6f 73 74 3a 20 74 70 66 6f
0040 72 65 76 65 72 2e 63 6f 6d 0d 0a 55 73 65 72 2d
0050 41 67 65 6e 74 3a 20 6c 65 61 72 6e 2d 6e 65 74
0060 77 6f 72 6b 69 6e 67 2f 31 2e 30 0d 0a 41 63 63
0070 65 70 74 3a 20 2a 2f 2a 0d 0a 43 6f 6e 6e 65 63
0080 74 69 6f 6e 3a 20 63 6c 6f 73 65 0d 0a 0d 0a分层解析
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第一层:IP Header(偏移 0x00,长度 20 字节)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
45 Version=4, IHL=5(头部20字节)
00 服务类型
00 93 总长度 = 147 字节(整个包)
51 81 标识符
40 00 Don't Fragment
80 TTL = 128(Windows 客户端)
06 Protocol = 6(TCP)
0e 1f IP 校验和
c0 a8 09 37 源 IP = 192.168.9.55
92 be 3e 27 目标 IP = 146.190.62.39
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第二层:TCP Header(偏移 0x14,长度 20 字节)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
d5 f7 源端口 = 54775
00 50 目标端口 = 80
85 12 8e c5 序号(相对值 1,握手后第一个数据包)
13 21 09 c6 确认号(相对值 1,期待服务器发 seq=1)
50 数据偏移 = 5(头部20字节,无选项)
18 Flags = 0x18 = 0001 1000
┌─────┬─────┬─────┬─────┬─────┬─────┐
│ URG │ ACK │ PSH │ RST │ SYN │ FIN │
│ 0 │ 1 │ 1 │ 0 │ 0 │ 0 │ ← PSH=1, ACK=1
└─────┴─────┴─────┴─────┴─────┴─────┘
ff 85 窗口大小 = 65413
00 00 TCP 校验和(此处省略实际值)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
第三层:HTTP Payload(偏移 0x28,长度 107 字节)
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
47 45 54 20 2f 20 48 54 54 50 2f 31 2e 31 0d 0a
↑ G E T 空格 / 空格 H T T P / 1 . 1 \r \n
→ "GET / HTTP/1.1\r\n" ← 请求行结束
48 6f 73 74 3a 20 74 70 66 6f 72 65 76 65 72 2e 63 6f 6d 0d 0a
↑ H o s t : 空格 t p f o r e v e r . c o m \r \n
→ "Host: tpforever.com\r\n"
55 73 65 72 2d 41 67 65 6e 74 3a 20 ...
→ "User-Agent: learn-networking/1.0\r\n"
41 63 63 65 70 74 3a 20 2a 2f 2a 0d 0a
→ "Accept: */*\r\n"
43 6f 6e 6e 65 63 74 69 6f 6e 3a 20 63 6c 6f 73 65 0d 0a
→ "Connection: close\r\n"
0d 0a
→ "\r\n" ← 空行,HTTP Header 在这里结束!Body 从下一个字节开始
(GET 请求没有 Body,所以包在这里结束)HTTP 请求的结构拆解
┌─────────────────────────────────────────────────────────────┐
│ 请求行(Request Line) │
│ GET / HTTP/1.1\r\n │
│ ↑ ↑ ↑ │
│ 方法 路径 版本 │
├─────────────────────────────────────────────────────────────┤
│ 请求头(Headers)—— 每行格式:Key: Value\r\n │
│ │
│ Host: tpforever.com\r\n ← HTTP/1.1 必须有 │
│ User-Agent: learn-networking/1.0\r\n ← 客户端标识 │
│ Accept: */*\r\n ← 接受任何响应格式 │
│ Connection: close\r\n ← 响应后关闭连接 │
├─────────────────────────────────────────────────────────────┤
│ 空行 \r\n ← 这是 Header 和 Body 的分界线,非常重要! │
│ 服务器靠这个知道 Header 在哪里结束 │
├─────────────────────────────────────────────────────────────┤
│ 请求体(Body) │
│ (GET 请求没有 Body,POST 请求的表单数据在这里) │
└─────────────────────────────────────────────────────────────┘关键细节:
| 细节 | 说明 |
|---|---|
每行用 \r\n 结尾 |
HTTP 规范要求,\r=0x0D,\n=0x0A |
空行 \r\n\r\n |
Header 结束的唯一标志,服务器解析时找这个 |
Host 必须有 |
HTTP/1.1 强制要求,一个 IP 可能托管多个域名 |
Connection: close |
告诉服务器"响应完就关连接",方便接收完整响应 |
用 Python 手写这个请求:
import socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.connect(("tpforever.com", 80)) # 触发三次握手
# 这就是上面那 107 字节的 HTTP 数据
request = (
"GET / HTTP/1.1\r\n"
"Host: tpforever.com\r\n"
"User-Agent: learn-networking/1.0\r\n"
"Accept: */*\r\n"
"Connection: close\r\n"
"\r\n" # 空行:Header 结束
)
sock.send(request.encode("utf-8"))7. HTTP 响应包:收到的是什么
服务器收到 GET 请求后返回响应。如果页面比较大,TCP 会把它拆成多个包分批发送。
响应被拆包的原因
握手时客户端声明 MSS=1360(最大数据段大小)
服务器不会发超过 1360 字节的 TCP 数据段
假设响应总共 5000 字节:
包1:Seq=1, Len=1360 → 传输字节 1~1360
包2:Seq=1361, Len=1360 → 传输字节 1361~2720
包3:Seq=2721, Len=1360 → 传输字节 2721~4080
包4:Seq=4081, Len=920 → 传输字节 4081~5000(最后一片)
每个包的 Seq = 上一个包的 Seq + 上一个包的 Len
接收方按 Seq 重新排列,拼成完整响应第一个响应包的字节结构
原始字节(前 60 字节,IP+TCP Header 部分):
0000 45 00 05 78 00 01 40 00 2e 06 ac ba 92 be 3e 27
0010 c0 a8 09 37 00 50 d5 f7 13 21 09 c6 85 12 8e c5
0020 50 18 fa f0 xx xx 00 00
━━━ IP Header ━━━
45 00 05 78 Version=4, 总长度=0x0578=1400字节(IP头20+TCP头20+数据1360)
2e TTL=46(服务器 Linux)
06 Protocol=TCP
92 be 3e 27 源 IP = 146.190.62.39(服务器)
c0 a8 09 37 目标 IP = 192.168.9.55(客户端)
━━━ TCP Header ━━━
00 50 源端口 = 80
d5 f7 目标端口 = 54775
13 21 09 c6 序号(相对值 1,服务器第一个数据包)
85 12 8e c5 确认号(相对值 1,确认收到了 GET 请求)
50 数据偏移 = 5(头部20字节)
18 Flags = 0x18 → [PSH, ACK]
fa f0 窗口大小 = 64240
━━━ HTTP Payload(从偏移 0x28 开始,1360 字节)━━━
48 54 54 50 2f 31 2e 31 20 32 30 30 20 4f 4b 0d 0a
↑ H T T P / 1 . 1 空格 2 0 0 空格 O K \r \n
→ "HTTP/1.1 200 OK\r\n" ← 状态行
43 6f 6e 74 65 6e 74 2d 54 79 70 65 3a 20 ...
→ "Content-Type: text/html; charset=utf-8\r\n"
... 更多 Headers ...
0d 0a
→ "\r\n" ← 空行,Header 在这里结束,Body 从下一字节开始
3c 68 74 6d 6c 3e ...
→ "<html>..." ← HTML Body 开始HTTP 响应的结构拆解
┌─────────────────────────────────────────────────────────────┐
│ 状态行(Status Line) │
│ HTTP/1.1 200 OK\r\n │
│ ↑ ↑ ↑ │
│ 版本 状态码 原因短语 │
├─────────────────────────────────────────────────────────────┤
│ 响应头(Headers) │
│ │
│ Content-Type: text/html; charset=utf-8\r\n ← Body 的格式 │
│ Content-Length: 5432\r\n ← Body 的长度 │
│ Server: nginx/1.18.0\r\n ← 服务器信息 │
│ Connection: close\r\n ← 发完就关连接 │
├─────────────────────────────────────────────────────────────┤
│ 空行 \r\n ← Header 结束,Body 从这里之后开始 │
├─────────────────────────────────────────────────────────────┤
│ 响应体(Body) │
│ <!DOCTYPE html> │
│ <html> │
│ <head>...</head> │
│ <body>...</body> │
│ </html> │
└─────────────────────────────────────────────────────────────┘如何知道 Body 在哪里结束?
HTTP 有两种方式告诉你 Body 的长度:
方式一:Content-Length(提前告知总长度)
HTTP/1.1 200 OK\r\n
Content-Length: 5432\r\n ← 直接告诉你 Body 是 5432 字节
\r\n
[5432 字节的 HTML]
→ 收够 5432 字节就结束
方式二:Transfer-Encoding: chunked(分块传输)
HTTP/1.1 200 OK\r\n
Transfer-Encoding: chunked\r\n ← 不知道总长度,分块发
\r\n
1a\r\n ← 块大小(十六进制)= 26 字节
[26 字节数据]\r\n
f0\r\n ← 下一块 240 字节
[240 字节数据]\r\n
0\r\n ← 块大小=0,表示结束
\r\n
→ 收到大小为 0 的块就结束用 Python 解析响应:
def parse_http_response(raw: bytes):
# 找到 \r\n\r\n,这是 Header 和 Body 的分界线
separator = b"\r\n\r\n"
header_end = raw.find(separator)
header_bytes = raw[:header_end] # Header 部分
body_bytes = raw[header_end + 4:] # Body 部分(跳过 4 字节的 \r\n\r\n)
# 解析状态行
lines = header_bytes.decode().split("\r\n")
status_line = lines[0] # "HTTP/1.1 200 OK"
version, status_code, reason = status_line.split(" ", 2)
# 解析 Headers
headers = {}
for line in lines[1:]:
if ": " in line:
key, value = line.split(": ", 1)
headers[key.lower()] = value
return status_code, headers, body_bytes常见 HTTP 状态码
| 状态码 | 含义 | 常见场景 |
|---|---|---|
200 OK |
成功 | 正常响应 |
301 Moved Permanently |
永久重定向 | HTTP 跳转到 HTTPS |
302 Found |
临时重定向 | 登录后跳转 |
304 Not Modified |
缓存有效 | 浏览器缓存命中,不返回 Body |
400 Bad Request |
请求格式错误 | 缺少 Host header |
403 Forbidden |
无权限 | 访问被拒绝 |
404 Not Found |
资源不存在 | 路径错误 |
500 Internal Server Error |
服务器内部错误 | 后端崩溃 |
8. TCP 四次挥手:关闭连接
由于请求头里有 Connection: close,服务器发完数据后主动发起关闭。
挥手过程
服务器 (146.190.62.39) 客户端 (192.168.9.55)
│ │
│ 第一步:FIN, ACK │
│ "我的数据发完了" │
│ Flags=[FIN,ACK] Seq=5441 Ack=108│
│──────────────────────────────────►│
│ │
│ 第二步:ACK │
│ "收到,知道你发完了" │
│ Flags=[ACK] Ack=5442 │
│◄──────────────────────────────────│
│ │
│ (客户端处理完所有数据) │
│ │
│ 第三步:FIN, ACK │
│ "我也发完了" │
│ Flags=[FIN,ACK] │
│◄──────────────────────────────────│
│ │
│ 第四步:ACK │
│ "收到,连接关闭" │
│──────────────────────────────────►│
│ │
│ 主动关闭方进入 TIME_WAIT │
│ 等待约 120 秒后彻底释放端口 │FIN 包的字节结构
原始字节(40 字节):
0000 45 00 00 28 xx xx 40 00 2e 06 xx xx 92 be 3e 27
0010 c0 a8 09 37 00 50 d5 f7 13 21 2b 42 85 12 8e c5
0020 50 11 fa f0 xx xx 00 00
━━━ TCP Header ━━━
00 50 源端口 = 80
d5 f7 目标端口 = 54775
13 21 2b 42 序号(相对值 5441,= 之前发送的所有数据字节数)
85 12 8e c5 确认号(相对值 108,确认收到了 GET 请求)
50 数据偏移 = 5(头部20字节)
11 Flags = 0x11 = 0001 0001
┌─────┬─────┬─────┬─────┬─────┬─────┐
│ URG │ ACK │ PSH │ RST │ SYN │ FIN │
│ 0 │ 1 │ 0 │ 0 │ 0 │ 1 │ ← FIN=1, ACK=1
└─────┴─────┴─────┴─────┴─────┴─────┘
━━━ Payload ━━━
(无,FIN 包不携带数据)为什么是四次,不是三次?
握手是三次:SYN-ACK 可以合并,因为服务器收到 SYN 后
立刻就能确认并发起自己的连接
挥手是四次:不能合并,因为:
收到对方 FIN 时,我可能还有数据没发完
必须先 ACK 对方的 FIN("知道你发完了")
等自己的数据全部发完,再发自己的 FIN
这两步之间可能有时间差,所以不能合并TIME_WAIT:为什么端口不能立刻复用
主动关闭方发出最后一个 ACK 后,进入 TIME_WAIT 状态
持续时间:2 × MSL(约 60~120 秒)
原因一:确保最后的 ACK 送达
如果最后的 ACK 丢了,对方会重发 FIN
TIME_WAIT 期间可以重新回复 ACK
如果直接关闭,重发的 FIN 会收到 RST,对方不知道连接是否正常关闭
原因二:让旧包自然消亡
网络中可能还有这条连接的旧包在路由器里漂着
等 2×MSL 后,这些旧包一定已经超时丢弃
新连接不会收到旧包的干扰
副作用:
TIME_WAIT 期间,相同的四元组(src_ip:port + dst_ip:port)不能复用
服务端用 SO_REUSEADDR 绕过这个限制,允许重启后立刻绑定同一端口9. IP Header 字段详解
IP Header 固定 20 字节(无选项时),负责把包从源地址路由到目标地址。
IP Header 结构(20 字节):
偏移 长度 字段名 含义
0 1 Version + IHL 高4位=版本(4=IPv4),低4位=头部长度(单位4字节)
1 1 DSCP/ECN 服务质量标记,普通流量填0
2 2 Total Length 整个IP包的总字节数(含头部)
4 2 Identification 分片标识,同一个包的分片有相同ID
6 2 Flags + Frag Offset 分片控制,DF=不分片,MF=还有更多分片
8 1 TTL 生存时间,每经过一个路由器减1,归零丢弃
9 1 Protocol 上层协议:6=TCP,17=UDP,1=ICMP
10 2 Header Checksum 头部校验和,接收方验证头部是否损坏
12 4 Source IP 源 IP 地址
16 4 Destination IP 目标 IP 地址TTL 的实际用途:
防止包在网络里无限循环:
每经过一个路由器,TTL 减 1
TTL=0 时,路由器丢弃包,并返回 ICMP "Time Exceeded" 消息
traceroute 的原理就是利用 TTL:
发 TTL=1 的包 → 第一跳路由器 TTL 变 0 → 返回 ICMP(暴露自己 IP)
发 TTL=2 的包 → 第二跳路由器返回 ICMP
...逐步探测出完整路径
通过 TTL 判断操作系统:
TTL ≈ 128 → Windows(初始值128)
TTL ≈ 64 → Linux / macOS(初始值64)
TTL ≈ 255 → 某些网络设备Protocol 字段常见值:
| 值 | 协议 | 说明 |
|---|---|---|
| 1 | ICMP | ping、traceroute 用的协议 |
| 6 | TCP | 可靠传输,HTTP/HTTPS/SSH 等 |
| 17 | UDP | 不可靠但快,DNS/视频流等 |
10. TCP Header 字段详解
TCP Header 最小 20 字节,有选项时最多 60 字节。
TCP Header 结构(最小 20 字节):
偏移 长度 字段名 含义
0 2 Source Port 发送方端口号(0~65535)
2 2 Destination Port 接收方端口号
4 4 Sequence Number 序号,这个包第一个字节的编号
8 4 Acknowledgment 确认号,期待对方下一个包的序号
12 1 Data Offset 头部长度(单位4字节),同时也是 Payload 的起始偏移
13 1 Flags 6个标志位(URG/ACK/PSH/RST/SYN/FIN)
14 2 Window Size 接收窗口大小,流量控制用
16 2 Checksum 校验和
18 2 Urgent Pointer 紧急数据偏移(URG=1时有效)
20+ 0~40 Options 可选项(MSS/WS/SACK等),握手时常见端口号的分类:
0~1023: 知名端口(Well-known),需要管理员权限绑定
80=HTTP,443=HTTPS,22=SSH,53=DNS,25=SMTP
1024~49151:注册端口,常见应用使用
3306=MySQL,5432=PostgreSQL,6379=Redis
49152~65535:临时端口(Ephemeral),OS 随机分配给客户端
你在抓包里看到的 54775 就是这类TCP Options 常见选项:
MSS(Maximum Segment Size)
Kind=2,握手时协商,决定每个 TCP 数据段的最大字节数
双方取较小值作为实际 MSS
例:客户端 MSS=1360,服务器 MSS=1410 → 实际用 1360
Window Scale(WS)
Kind=3,握手时协商,扩大窗口大小
实际窗口 = Window Size × 2^Scale
例:Win=64240,Scale=7 → 实际窗口 = 64240 × 128 = 8,222,720 字节
SACK(Selective Acknowledgment)
Kind=4(握手时声明支持),Kind=5(实际使用时携带数据)
丢包时只重传丢失的包,不用重传整个窗口
大幅提升高丢包率网络的性能
Timestamps
Kind=8,用于精确计算 RTT(往返时延)和防止序号回绕11. 关键概念速查表
整个流程的包序列
方向 包类型 Flags Seq Ack Len 说明
客户端→服务器 SYN [SYN] 0 - 0 握手第一步
服务器→客户端 SYN-ACK [SYN,ACK] 0 1 0 握手第二步
客户端→服务器 ACK [ACK] 1 1 0 握手第三步,连接建立
客户端→服务器 HTTP GET [PSH,ACK] 1 1 107 发送请求
服务器→客户端 ACK [ACK] 1 108 0 确认收到请求
服务器→客户端 HTTP 响应片1 [ACK] 1 108 1360 响应第1片
服务器→客户端 HTTP 响应片2 [ACK] 1361 108 1360 响应第2片
服务器→客户端 HTTP 响应片3 [PSH,ACK] 2721 108 ... 响应最后一片
客户端→服务器 ACK [ACK] 108 5442 0 确认收到响应
服务器→客户端 FIN,ACK [FIN,ACK] 5441 108 0 服务器关闭
客户端→服务器 ACK [ACK] 108 5442 0 确认关闭
客户端→服务器 FIN,ACK [FIN,ACK] 108 5442 0 客户端关闭
服务器→客户端 ACK [ACK] 5442 109 0 连接完全关闭核心概念一句话总结
| 概念 | 一句话 |
|---|---|
| Socket | 操作系统给进程分配的文件描述符,网络通信就是两个进程互相读写文件 |
| TCP 三次握手 | 双方交换初始序号,确认对方能收能发,才开始传数据 |
| 序号(Seq) | 每个字节都有编号,接收方靠序号重排乱序包、检测丢包 |
| 确认号(Ack) | "我期待你下一个发的序号是 X",X 之前的字节我都收到了 |
| MSS | 握手时协商的最大数据段大小,大包会被拆成 MSS 大小的片 |
| TTL | 每经过一个路由器减 1,防止包无限循环;也用来判断操作系统类型 |
| PSH | 告诉 OS 立刻把数据交给应用层,不要等缓冲区满 |
| FIN | 优雅关闭,"我发完了,等你也发完" |
| RST | 强制关闭,"连接立刻终止" |
| TIME_WAIT | 主动关闭方等待 2×MSL,确保最后的 ACK 送达,让旧包消亡 |
| HTTP 空行 | \r\n\r\n 是 Header 和 Body 的唯一分界线 |
| DNS TTL | 记录的缓存时间,这是"改了 DNS 要等生效"的根本原因 |
Wireshark 常用过滤器
tcp.stream eq 25 只看第25条 TCP 流(同一连接的所有包)
tcp.flags.syn == 1 只看 SYN 包
tcp.flags.fin == 1 只看 FIN 包
http 只看 HTTP 流量
tcp.port == 80 只看端口 80 的流量
ip.addr == 192.168.9.55 只看某个 IP 的流量
tcp.analysis.retransmission 只看重传包(排查丢包问题)
