bigywater.tech
网络协议 · 37

TCP 头部逐字段:序号、确认号、标志位与选项

TCP 头里每个字段到底在干什么?序号、确认号怎么对应?SYN 里那串选项各是什么?这篇在 en0 上抓一次真实的 HTTP 连接,逐个包逐字段解码,再用 lo0 看标志位的分布和已经被淘汰的 URG。

44 字节
真实 SYN 的 TCP 头长度:20 字节固定 + 24 字节选项(MSS、窗口缩放、时间戳、SACK-permitted)
92 / 98
lo0 上下载 1 MiB 时纯 ACK 包占的个数;只有握手的 SYN 没有 ACK 位
16332 = 16344 − 12
数据包的最大载荷:MSS 减去每个包都带的 12 字节时间戳选项

固定头

实测 · 本机执行输出
    #  方向   源端口→目的端口  相对序号  相对确认号  头长  标志        窗口    载荷  校验和
     1  →   C→80                  0          0   44  CWR+ECE+SYN   65535      0  0x3e01
     2  ←   80→C                   0          1   40  ECE+ACK+SYN   65535      0  0x98e8
     3  →   C→80                  1          1   32  ACK           2061      0  0xbef2
     4  →   C→80                  1          1   32  ACK+PSH       2061     56  0x4c99
     5  ←   80→C                   1         57   32  ACK             16      0  0xc5fa
     6  ←   80→C                   1         57   32  ACK+PSH         16    923  0x5171
     7  ←   80→C                 924         57   32  ACK+PSH         16      5  0x7e2f
     8  →   C→80                 57        929   32  ACK           2047      0  0xb99c
     9  ←   80→C                 929         57   32  ACK+FIN         16      0  0xc250
  整个连接共 12 个包。每个包的头都是 20 字节固定部分 + 选项:源端口 2、目的端口 2、序号 4、确认号 4、数据偏移 4 位 + 保留 4 位(合 1 字节)、标志 1 字节(CWR ECE URG ACK PSH RST SYN FIN)、窗口 2、校验和 2、紧急指针 2
  第 1 个包(SYN)的头 20 字节:c080 0050 7432 eaec 0000 0000 b0c2 ffff 3e01 0000;数据偏移 = 11(×4 = 44 字节),保留位 0,标志字节 0xc2

源端口、目的端口、序号、确认号、数据偏移、标志、窗口、校验和、紧急指针,共 20 字节。SYN 和 FIN 各占一个序号,所以 ACK 的确认号比对方的序号多 1。

选项

实测 · 本机执行输出
  SYN 的选项 24 字节:02 04 05 b4 01 03 03 06 01 01 08 0a 5f 0a 2f 86 00 00 00 00 04 02 00 00
    MSS(类型 2,长度 4):MSS = 1460
    NOP(1 字节,用来对齐 / 结束)
    WScale(类型 3,长度 3):窗口缩放移位 6(窗口字段 × 2^6)
    NOP(1 字节,用来对齐 / 结束)
    NOP(1 字节,用来对齐 / 结束)
    Timestamps(类型 8,长度 10):TSval = 1594503046,TSecr = 0
    SACK-permitted(类型 4,长度 2):允许选择性确认
    EOL(1 字节,用来对齐 / 结束)
  SYN-ACK 的选项 20 字节:MSS、SACK-permitted、Timestamps、NOP、WScale
  带数据的包头长 32 字节(选项 ['NOP', 'NOP', 'Timestamps'])、32 字节(选项 ['NOP', 'NOP', 'Timestamps'])
  各种包的头长:{44: 1, 40: 1, 32: 10}——SYN 的头最长(选项最多),之后只剩时间戳(NOP NOP + 10 字节 = 12 字节),头 32 字节

SYN 里一次性协商 MSS、窗口缩放、时间戳、SACK-permitted;之后每个包只保留时间戳(12 字节)。数据偏移字段告诉接收方头有多长。

标志位

实测 · 本机执行输出
  下载 1048576 字节,lo0 上共 98 个 TCP 包;标志组合:ACK × 92;ACK+PSH × 2;SYN × 1;ACK+SYN × 1;ACK+PSH+FIN × 1;ACK+FIN × 1
  窗口字段(16 位原始值):客户端发的 SYN 65535,之后客户端的 ACK 里最小 4085 / 最大 26523;SYN 的窗口字段不缩放(窗口缩放选项只对后面的包有效)
  服务端发出的数据包:载荷最大 16332 字节(= MSS 16344),PSH 位出现 3 次——PSH 只是提示接收方尽快交给应用,不改变传输

握手之后每个包都带 ACK;PSH 只是提示;FIN 与 RST 结束连接。lo0 下载 1 MiB 的 98 个包里 92 个是纯 ACK。

URG

实测 · 本机执行输出
    客户端发出:标志 ACK+PSH,载荷 3 字节,紧急指针字段 0
    客户端发出:标志 URG+ACK+PSH,载荷 1 字节,紧急指针字段 1
    客户端发出:标志 ACK+PSH,载荷 3 字节,紧急指针字段 0
  接收方 recv(MSG_OOB) 拿到 b'!';普通 recv 拿到 b'abc'
  URG 标志置位时,紧急指针指向紧急数据的末尾(相对序号偏移);它几乎没有被实际用过,而且“带外数据”在各系统里语义不一致,RFC 6093 建议新应用不要使用

紧急指针指向紧急数据的位置,MSG_OOB 发出的包能看到。语义不统一,不建议使用。

分步演示

TCP 头部
本步的对象

    本文用到的工具与边界

    • 本机 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,数字每次运行会有小幅抖动。第一、二节是 en0 上 example.com 的一次真实 HTTP 请求(序号用相对值,端口用 C 表示本机的临时端口);第三、四节在 lo0 上做;窗口字段的真实窗口要乘以缩放系数(本篇只展示原始字段)
    • 没有验证:URG 在不同系统上的语义差别;选项的更多种类(TFO、MPTCP、MD5 签名等);ECN 位(CWR / ECE)的实际工作(本机 SYN 带 CWR+ECE 是协商 ECN);窗口缩放对服务器方向(窗口字段 16)的换算(服务器的缩放系数没有读)
    京ICP备2026067502号-1京公网安备11010502063487号