一次 curl 请求到底发生了什么?Wireshark 抓包逐帧拆解

0 阅读9分钟

从一次 curl 抓包,彻底搞懂 HTTP 请求的完整生命周期

前言:很多人学网络协议都是"背概念",但如果你亲手抓一次包,把 DNS、TCP 三次握手、HTTP 请求/响应、TCP 四次挥手全部看一遍,那些概念就真的活了。本文通过一次真实的 curl http://www.baidu.com 抓包,逐包拆解整个网络通信过程。

一、抓包环境

操作系统:macOS
抓包工具:Wireshark
命令:curl http://www.baidu.com
客户端 IPv6:2409:8a00:3133:1:3486:518f:3418:d42b
百度服务端 IPv6:2409:8c00:6c21:11eb:0:ff:b0bf:59ca

提示:因为系统优先使用 IPv6,所以 curl 通过 IPv6 和百度通信。如果你只想看 IPv4,可以用 curl -4 http://www.baidu.com

抓包时在 Wireshark 显示过滤器中输入 ip.addr == 2409:8c00:6c21:11eb:0:ff:b0bf:59ca or dns,可以过滤掉无关流量。


二、第一步:DNS 查询 —— "百度在哪?"

在发 HTTP 请求之前,浏览器(curl)需要先知道 www.baidu.com 对应的 IP 地址。这一步由 DNS 协议完成。

2.1 发出 DNS 查询

curl 同时发出了两个 DNS 查询:

No.87  22:33:11.941  客户端 → 路由器
      DNS Standard query A www.baidu.com
      (查询 IPv4 地址)

No.88  22:33:11.941  客户端 → 路由器
      DNS Standard query AAAA www.baidu.com
      (查询 IPv6 地址)
  • A 记录:查询域名对应的 IPv4 地址
  • AAAA 记录:查询域名对应的 IPv6 地址(因为名称里有四个 A,代表比 A 记录更长的地址)

两个查询在同一毫秒发出,这就是所谓的 Happy Eyeballs(快乐眼球) 算法 —— 同时查询 IPv4 和 IPv6,谁先回来就用谁。

2.2 收到 DNS 响应

No.92  22:33:11.964  路由器 → 客户端
      DNS Standard query response A www.baidu.com CNAME www.a.shifen.com
      A 39.156.70.46
      A 39.156.70.239

No.93  22:33:11.967  路由器 → 客户端
      DNS Standard query response AAAA www.baidu.com CNAME www.a.shifen.com
      AAAA 2409:8c00:6c21:11eb:0:ff:b0bf:59ca
      AAAA 2409:8c00:6c21:118b:0:ff:b0e8:f003

这里有一个细节值得注意:

  • www.baidu.com 并没有直接返回 IP,而是通过 CNAME 指向了 www.a.shifen.com(百度的 CDN 域名)
  • 最终返回了 两个 IPv4 + 两个 IPv6 地址,这是为了负载均衡和高可用

⏱️ DNS 查询耗时:23ms(从 11.941 到 11.964)

DNS 小结

客户端                          路由器(DNS)
  │                                 │
  │── A 记录查询 ─────────────────→│
  │── AAAA 记录查询 ──────────────→│
  │                                 │
  │←── 返回 IPv4 地址 ────────────│
  │←── 返回 IPv6 地址 ────────────│
  │                                 │
  │  ✅ 拿到百度的 IP 地址了!       │

三、第二步:TCP 三次握手 —— "建立连接"

拿到 IP 后,curl 需要和百度服务器建立一个可靠的 TCP 连接。这个过程就是经典的 三次握手

No.94  22:33:11.968  客户端 → 百度:80
      [SYN, ECE, CWR] Seq=0 Win=65535 Len=0
      MSS=1440 WS=64 SACK_PERM

No.95  22:33:11.978  百度:80 → 客户端
      [SYN, ACK, ECE, CWR] Seq=0 Ack=1 Win=8192 Len=0
      MSS=1380 WS=32 SACK_PERM

No.96  22:33:11.978  客户端 → 百度:80
      [ACK] Seq=1 Ack=1 Win=262144 Len=0

逐包解读:

第一次握手 [SYN]:

  • SYN = Synchronize,"我要和你建立连接"
  • Seq=0:序列号从 0 开始
  • MSS=1440:我这边能接收的最大分段大小是 1440 字节(IPv6 比 IPv4 的 1460 小一些)
  • WS=64:Window Scale = 64,窗口缩放因子(用于 TCP 窗口扩大)
  • SACK_PERM:允许选择性确认

第二次握手 [SYN, ACK]:

  • SYN:我也想和你建立连接
  • ACK=1:我确认收到了你的 SYN(Ack = 你的 Seq + 1)
  • MSS=1380:百度那边的最大分段是 1380 字节
  • WS=32:百度的窗口缩放因子是 32

第三次握手 [ACK]:

  • ACK=1:确认收到百度的 SYN
  • Win=262144:我现在的接收窗口是 262144 字节(比最初的 65535 大了,因为 WS 协商后窗口扩大了)

⏱️ 三次握手耗时:1.6ms(11.968 → 11.978)

💡 你可能注意到标志位里有 ECE, CWR,这是 ECN(显式拥塞通知),表示双方都支持在路由器拥塞时提前降速,而不是等到丢包才处理。

三次握手小结

客户端                           百度服务器
  │                                │
  │──── SYN (Seq=0) ────────────→ │  ① 你好,我要连接你
  │                                │
  │←─── SYN+ACK (Ack=1) ─────────│  ② 好的,我也准备好了
  │                                │
  │──── ACK (Ack=1) ────────────→ │  ③ 确认,连接建立!
  │                                │
  │     ✅ TCP 连接建立成功          │

四、第三步:HTTP 请求 —— "给我首页"

连接建立后,curl 发出 HTTP 请求:

No.97  22:33:11.978  客户端  百度:80
      GET / HTTP/1.1
      Host: www.baidu.com
      User-Agent: curl/8.7.1
      Accept: */*

展开这个包,你会看到完整的 HTTP 请求头:

GET / HTTP/1.1\r\n
Host: www.baidu.com\r\n
User-Agent: curl/8.7.1\r\n
Accept: */*\r\n
\r\n

这就是 HTTP 请求的全部内容 —— 一个 GET 方法,请求根路径 /,使用 HTTP/1.1 协议。非常简洁。

💡 注意最后一个 \r\n\r\n(空行),这是 HTTP 协议规定的请求头结束标志


五、第四步:HTTP 响应 —— 百度返回首页

百度收到请求后,开始返回 HTML 内容。因为百度首页约 617KB,TCP 需要把它拆成很多小包来传输。

5.1 数据分片传输

No.101  22:33:11.996  百度 → 客户端
      [ACK] Seq=1 Ack=77 Len=1360
      [TCP PDU reassembled in 609]

No.103  22:33:11.998  百度 → 客户端
      [ACK] Seq=1361 Ack=77 Len=1360
      [TCP PDU reassembled in 609]

No.105  22:33:11.999  百度 → 客户端
      [ACK] Seq=2721 Ack=77 Len=1360
      [TCP PDU reassembled in 609]

解读:

  • 每个包携带 1360 字节数据(受 IPv6 MSS=1380 限制)
  • Seq 递增:1 → 1361 → 2721(每次加 1360)
  • Ack=77 始终不变:表示百度已经收到了客户端的 77 字节(HTTP 请求的大小)
  • TCP PDU reassembled in 609:这是 Wireshark 的关键提示!意思是"这个包里的数据只是第 609 号完整数据的一部分"

5.2 为什么有这么多包?

从 No.101 到 No.608,百度连续发送了约 460 个 TCP 包来传输 617KB 的 HTML。这是正常的 —— TCP 就像一个快递员,货物太大就分成很多小包裹来送。

5.3 最终的 HTTP 响应

No.609  22:33:12.083  百度 → 客户端
      HTTP/1.1 200 OK (text/html)

在 Wireshark 中右键这个包 → Follow → TCP Stream,就能看到完整的 HTTP 响应:

HTTP/1.1 200 OK
Accept-Ranges: bytes
Cache-Control: max-age=86400
Connection: keep-alive
Content-Length: 631716
Content-Type: text/html
Date: Mon, 06 Apr 2026 14:33:11 GMT
Etag: "..."
Last-Modified: Mon, 23 Jun 2025 02:52:02 GMT
Server: BWS/1.1

<!DOCTYPE html>
<html>...(百度首页 HTML)

关键字段解读:

响应头含义
HTTP/1.1 200 OK请求成功
Content-Type: text/html返回的是 HTML 页面
Content-Length: 631716响应体大小约 617KB
Server: BWS/1.1百度自研 Web 服务器
Cache-Control: max-age=86400告诉客户端可以缓存 86400 秒(1天)
Connection: keep-aliveTCP 连接保持打开(可以复用)

⏱️ 整个数据传输耗时:87ms(11.996 → 12.083)


六、第五步:TCP 四次挥手 —— "连接关闭"

HTTP 响应传完后,虽然 Connection: keep-alive 表示连接可以复用,但 curl 默认请求完就关闭连接。

No.612  22:33:12.084  客户端 → 百度:80
      [FIN, ACK] Seq=77 Ack=631794 Win=1462784 Len=0

No.613  22:33:12.093  百度:80 → 客户端
      [ACK] Seq=631794 Ack=78 Win=78848 Len=0

No.614  22:33:12.096  百度:80 → 客户端
      [FIN, ACK] Seq=631794 Ack=78 Win=78848 Len=0

No.615  22:33:12.096  客户端 → 百度:80
      [ACK] Seq=78 Ack=631795 Win=1462784 Len=0

逐包解读:

方向含义
No.612客户端 → 百度FIN:我说完了,准备关闭(Seq=77,和之前 HTTP 请求衔接)
No.613百度 → 客户端ACK:知道了,我先确认(此时百度可能还有数据要发)
No.614百度 → 客户端FIN:我也说完了,可以关闭了
No.615客户端 → 百度ACK:确认,连接正式关闭

⏱️ 四次挥手耗时:12ms(12.084 → 12.096)

💡 为什么需要四次而不是三次?因为 TCP 是全双工的(双方可以同时发送数据),所以每一方都需要独立地通知对方"我说完了"。


七、全景时间线

把所有关键节点串在一起:

时间戳                事件                           耗时
─────────────────────────────────────────────────────────
22:33:11.941     DNS 查询发出                      
22:33:11.964     DNS 响应收到                      23ms
22:33:11.968     TCP SYN(第一次握手)              
22:33:11.978     TCP SYN+ACK(第二次握手)           1.6ms
22:33:11.978     TCP ACK(第三次握手)              0ms
22:33:11.978     HTTP GET 请求发出                  0ms
22:33:11.996     百度开始传输 HTML                  
22:33:12.083     HTML 传输完成(HTTP 200 OK)      87ms
22:33:12.084     TCP FIN(开始关闭连接)            
22:33:12.096     TCP 连接完全关闭                  12ms
─────────────────────────────────────────────────────────
                 总耗时                           ~155ms

从 DNS 查询到连接关闭,一次完整的 HTTP 请求只用了 155 毫秒


八、知识总结

一次 HTTP 请求涉及的协议栈

应用层:   DNS(域名解析)→ HTTP(请求/响应)
传输层:   TCP(三次握手、可靠传输、四次挥手)
网络层:   IPv6 / IPv4(路由寻址)
网络接口层: 以太网 / Wi-Fi(物理传输)

你应该记住的关键点

  1. DNS 先行:任何 HTTP 请求之前,必须先通过 DNS 将域名解析为 IP
  2. TCP 保证可靠:三次握手建立连接,数据传输有序列号和确认,四次挥手关闭连接
  3. HTTP 是明文http:// 下的请求和响应都是明文传输,抓包可以直接看到内容。https:// 则需要 TLS 加密
  4. 大数据分片:HTTP 响应太大时,TCP 会拆成多个包传输,Wireshark 会自动标记 TCP PDU reassembled
  5. curl 的行为:curl 用完就关(发 FIN),即使服务器说 keep-alive。浏览器则会复用连接以提高性能

动手建议

如果你想继续深入,可以尝试以下实验:

# 对比 HTTP 和 HTTPS 的区别
curl http://www.baidu.com        # 明文,能看到请求和响应内容
curl https://www.baidu.com       # 加密,只能看到 TLS 握手和加密数据

# 对比 GET 和 POST
curl -X POST -d "key=value" http://httpbin.org/post

# 看重定向过程
curl http://baidu.com            # 会 302 跳转到 www.baidu.com

# 模拟慢速网络(配合 Wireshark 观察 TCP 重传)
sudo ipconfig set en0 mtu 576    # 降低 MTU,观察分片变化

抓包不是目的,理解协议才是。 当你真正理解了 DNS → TCP → HTTP 这个链路,很多网络问题(DNS 劫持、连接超时、TLS 握手失败、页面加载慢)都能从抓包中找到线索。建议打开 Wireshark 亲手操作一遍,对照着本文逐包查看,效果最好。