bigywater.tech
网络协议 · 1

分层与封装:一个 HTTP 请求在线上到底多了哪几层头

一个 HTTP 请求在 socket 里只是 82 字节的文本,可到了网线上它被包了好几层头。每一层多出多少字节、头里的字段是什么、小请求和大下载的头部占比差多少?这篇用 tcpdump 抓真实的包,再用代码按字节拼一遍来对照。

82 → 138
82 字节的 HTTP 请求在 lo0 上变成 138 字节(4 + 20 + 32 + 82)
14 B
真实以太网比环回多出的以太网头;MSS 随之从 16344 变成 1460
0.35% vs 4.36%
1 MiB 下载的头部占比:lo0 实测与以太网按 MSS 1448 的公式值

应用层只是字节

实测 · 本机执行输出
  收到 82 字节: b'GET /hello HTTP/1.1\r\nHost: 127.0.0.1:9403\r\nUser-Agent: curl/8.7.1\r\nAccept: */*\r\n\r\n'

HTTP 请求在 socket 里就是一段文本,TCP 不理解它,只负责可靠地搬运字节流。

逐层的头

实测 · 本机执行输出
  这个会话共抓到 14 个包(握手 3 + 请求 1 + 响应 1 + 挥手若干 + ACK……),其中携带请求的是:
  lo0 链路层头: 4 字节(地址族 AF_INET=2,小端存放),没有 MAC 地址
  IPv4 头: 20 字节  版本=4 头长=5x4 TOS=0 总长=134 id=0 标志=DF(True) TTL=64 协议=6(TCP) 校验和=0x0000 127.0.0.1 → 127.0.0.1
  TCP 头: 32 字节  源端口=客户端临时端口 目的端口=9400 seq 相对=1 标志=PSH ACK 窗口=6380 选项长度=12 字节
  应用数据(HTTP 请求): 82 字节
  合计: 4(lo0) + 20(IP) + 32(TCP) + 82(HTTP) = 138 字节;头部开销占 39% (IP 总长 134)
  这个包的前 64 字节(从 IP 头开始):
    0000  45 00 00 86 00 00 40 00 40 06 00 00 7f 00 00 01  E.....@.@.......
    0010  7f 00 00 01 f3 8b 24 b8 5e 79 0d fd 5f 47 4a 5e  ......$.^y.._GJ^
    0020  80 18 18 ec fe 7a 00 00 01 01 08 0a 0d 54 0f 28  .....z.......T.(
    0030  bc a3 b1 f9 47 45 54 20 2f 68 65 6c 6c 6f 20 48  ....GET /hello H

IPv4 头 20 字节(版本、头长、总长、TTL、协议号、校验和、地址),TCP 头 20 字节加选项(这里是 12 字节的时间戳选项,共 32 字节),链路层头在环回接口上是 4 字节,在以太网上是 14 字节(目的 MAC、源 MAC、EtherType)。

MTU 决定一个包装多少

实测 · 本机执行输出
  curl 返回 HTTP 200;这个会话共抓到 12 帧
  以太网头: 14 字节  目的 MAC=70:85:6c:xx:xx:xx(默认网关) 源 MAC=7e:aa:6f:xx:xx:xx(本机) EtherType=0x0800(IPv4)
  IPv4 头: 20 字节  TTL=64 协议=6 总长=126 校验和=0x3036 目的=172.66.147.243
  TCP 头: 32 字节  目的端口=80 选项长度=12 字节;HTTP 请求 74 字节
  整个帧: 140 字节 = 14 + 20 + 32 + 74;同一个请求在 lo0 上是 4 + 20 + 32 + 82
  en0 上 SYN 里的 MSS = [1460](= 1500 − 20 − 20);lo0 上是 16344(= 16384 − 20 − 20)

MSS = MTU − 20 − 20:环回 16344,以太网 1460。同样的 1 MiB,环回上只要 66 个带数据的包,以太网上要 725 个帧。

开销占比

实测 · 本机执行输出
  lo0:响应方向携带数据的包 66 个,应用数据共 1048719 字节(含 HTTP 响应头);最大的一个包的 TCP 数据 16332 字节
  这些包的头部合计 3696 字节,占总字节的 0.35%
  同样的 1048719 字节放到 MSS=1448 的以太网上:需要 725 个帧,头部 47850 字节(每帧 14 + 20 + 32),占 4.36%(这是按公式算出来的,不是实测)
  对比第二节那次只取 12 字节响应体的小请求:14 个包共 1040 字节(含 lo0 头),应用数据只有 232 字节,占 22%——小请求里绝大部分字节是头部和握手

大下载的头部占比很小(lo0 上 0.35%,以太网上约 4.4%),小请求则很高:14 个包里应用数据只占 22%。

校验和与卸载

实测 · 本机执行输出
  自己用 struct 拼出的 IPv4 头(校验和先填 0)与抓到的前 20 字节:除第 10~11 字节(校验和)外 完全相同
  抓到的原始字节里 IP 校验和字段 = 0x0000(环回口发送时没有填,tcpdump 提示 bad cksum);自己按反码求和算出来应该是 0x3c70
  所以在环回接口上看到的 “bad cksum” 不是出错:校验和被留空(校验和卸载),真正走网卡时才会填上

环回接口上抓到的包校验和是 0——发送时没有填,tcpdump 报 bad cksum 是校验和卸载的结果,不是错误。

分步演示

分层与封装
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64(Apple 芯片)/ Python 3.14.8 / tcpdump(libpcap)/ scapy 2.8 / dpkt / dnspython / h2 / aioquic / OpenSSL / curl 8.7.1;抓包在 lo0(环回)上做,少数实验用真实网卡 en0(这台机器上 en0 是 Wi-Fi,tcpdump 看到的是系统转成以太网格式的帧,所以文中的“以太网头”都指这个 14 字节的头)或访问公网(会在文章里注明);需要丢包 / 延迟 / 限速时,用一个只作用于 lo0 端口 9400-9499 的 dummynet 辅助脚本(netlab);所有服务端都是本机自己起的进程;时间戳取自 tcpdump 的 -tt,数字每次运行会有小幅抖动。第二、四、五节在 lo0 上抓包(tcpdump -w,用 dpkt 解析);第三节在 en0 上访问 example.com:80,MAC 地址只保留前 3 字节;第四节的以太网数字是按 MSS 1448 和每帧 66 字节头算出来的,没有实测
    • 没有验证:Wi-Fi(802.11)帧头;VLAN / PPPoE 等多出的头;IPv6;网卡开启 TSO / LRO 时抓到的“超大包”;不同 MTU(巨型帧、隧道)下的 MSS;TCP 选项在其它操作系统上的组合
    京ICP备2026067502号-1京公网安备11010502063487号