☰
TCP/IP/UDP报文格式详解:从抓包数据逐字段解析网络协议
2026/10/20 3:11:43 网站建设 项目流程

1. 为什么值得把报文格式啃透

抓包工具里那一串串十六进制,刚入行的时候我也看得头皮发麻。明明知道这是网络通信的"原始语言",可就是不知道从哪一位读到哪一位,哪个字段管什么用。后来做协议解析、写抓包分析脚本、排查线上诡异丢包问题,被逼着把 TCP、IP、UDP 这几种核心报文格式一位一位地啃了一遍,才发现这东西根本不是什么需要死记硬背的八股文,而是一张"通信双方约定的表格"——你只要理解了每一列存在的理由,格式自然就记住了。

这篇文章想做的事情很直接:把TCP、IP、UDP 等各种报文格式从"看起来像天书"拆成"能对着抓包数据逐字段读下来"的程度。我会从整体设计思路讲起,把 IP 头、TCP 头、UDP 头、ICMP 头这些核心结构逐字段拆解,配上真实的字段位宽、常见取值、以及我在实际排查中踩过的坑。适合正在学网络基础的同学、需要做协议解析的开发者、以及被抓包数据折磨过的运维和测试人员。读完你至少能做到:拿到一段十六进制报文,知道从第几个字节开始读什么,遇到异常字段能判断问题大概出在哪一层。

需要先说明一点:报文格式本身是公开的协议规范,我下面讲的所有字段定义都来自通用的网络协议标准,但具体的排查经验、参数取舍、工具使用技巧,是我在实际工作中积累的,带有个人实践色彩,你可以当作参考而不是唯一答案。

2. 报文格式的整体设计思路拆解

2.1 分层封装:像寄快递一样一层套一层

理解报文格式,最关键的一个心智模型是分层封装。你可以把它想象成寄快递:你要寄的东西是"数据",装进一个小盒子是"UDP 或 TCP 段",小盒子外面套一个标准纸箱是"IP 包",纸箱上再贴一张快递单是"以太网帧"。每一层只关心自己那一层的"面单信息",拆包的时候也是一层一层剥。

这个设计的好处非常明显。网络设备在处理数据时,不需要关心上层到底是什么内容。路由器只看 IP 头就能决定往哪转发,交换机只看以太网头就能决定从哪个口出去。每一层各司其职,任何一层换实现都不影响其他层。这也是为什么你换了个网卡、换了个运营商,上层应用几乎无感知——因为分层把变化隔离住了。

从格式角度看,每一层报文都是"固定头部 + 可变负载"的结构。头部里放的是这一层必须让对端或中间设备知道的控制信息,负载则是从上层原封不动搬下来的数据。理解这一点,你就明白为什么 IP 头是 20 字节起步、TCP 头也是 20 字节起步——这些字节不是随便定的,每一个都对应一个明确的功能需求。

2.2 为什么头部字段要按位来划分

很多人第一次看报文格式图会疑惑:为什么字段宽度是 4 位、3 位、13 位这种奇怪的数字,而不是整齐的 8 位、16 位?这背后是空间效率的考量。网络带宽在协议设计之初是极其宝贵的资源,每一个比特都要省着用。一个字段如果只需要表示 0 到 15 这 16 种状态,那就用 4 位,绝不多给。

比如 IP 头里的"首部长度"字段只有 4 位,因为 IP 头最长就是 60 字节,用 4 位表示"有多少个 4 字节单位"(最大 15 乘以 4 等于 60)就够了。再比如 TCP 头里的"首部长度"也是 4 位,同样的道理。这种"按需分配位宽"的思路贯穿整个协议设计,也是读报文时最容易卡住的地方——你必须知道每个字段占几位,才能正确地对齐到下一个字段。

提示:读报文格式图时,永远从左上角开始,按行从左到右读,一行读完再读下一行。位宽标注在字段上方或下方,单位是 bit(位),8 位等于 1 字节。

2.3 大端序:网络字节序的统一约定

还有一个绕不开的点是字节序。不同 CPU 架构存储多字节整数的方式不一样,有的把高位字节放前面(大端),有的把低位字节放前面(小端)。如果通信双方各按各的习惯来,读出来的端口号、序列号就全乱了。所以网络协议统一规定:所有超过 1 字节的整数字段,一律采用大端序传输,也就是高位字节在前。

这个约定叫"网络字节序"。你在写解析代码的时候,如果发现读出来的端口号是个离谱的大数字,八成就是忘了做字节序转换。比如端口 80,大端序存储是00 50,如果你按小端读就成了50 00,也就是 20480,完全对不上。这个坑我踩过不止一次,尤其是用某些语言直接按内存布局解析结构体的时候。

3. IP 报文格式逐字段拆解

3.1 IPv4 头部整体布局

IPv4 头部是理解所有上层协议的基石,因为 TCP、UDP、ICMP 全都要装在 IP 包里。它的标准长度是 20 字节,如果带了选项字段,最长可以到 60 字节。下面这张表是我整理的核心字段速查,位宽和含义都标清楚了。

字段名位宽作用说明
版本4 位值为 4 表示 IPv4,值为 6 表示 IPv6
首部长度4 位以 4 字节为单位,最小值 5(即 20 字节)
服务类型8 位用于 QoS,标记优先级和延迟要求
总长度16 位首部加数据的总字节数,最大 65535
标识16 位分片重组时用来判断哪些片属于同一个包
标志3 位含 DF(禁止分片)和 MF(还有更多分片)
片偏移13 位以 8 字节为单位,标记分片在原包中的位置
生存时间8 位每经过一个路由器减 1,减到 0 就丢弃
协议8 位6 表示 TCP,17 表示 UDP,1 表示 ICMP
首部校验和16 位只校验首部,不校验数据
源 IP 地址32 位发送方地址
目的 IP 地址32 位接收方地址

这张表建议你对着抓包工具里的实际数据看一遍,比死记强一百倍。我第一次真正记住"协议号 6 是 TCP、17 是 UDP",就是因为抓了一个包,看到协议字段是06,然后对照着确认了这就是 TCP。

3.2 首部长度和总长度:两个最容易混淆的字段

这两个字段名字很像,但作用完全不同,是新手最容易搞混的地方。首部长度说的是"IP 头本身有多长",单位是 4 字节。正常情况下没有选项,值是 5,也就是 20 字节。总长度说的是"整个 IP 包有多长",包括头部和数据,单位是字节。

为什么要区分?因为 IP 包的数据部分长度是不固定的,而头部长度在带选项时会变。接收方需要先读首部长度,知道头部到哪结束,剩下的就是数据。再读总长度,知道整个包到哪结束。如果总长度大于实际收到的字节数,说明包被截断了;如果小于,说明后面还有填充或者粘包。

注意:总长度字段是 16 位,最大只能表示 65535 字节。这就是为什么单个 IP 包不能超过 64KB,超过的数据必须靠上层(比如 TCP)分段,或者靠 IP 分片机制处理。

3.3 分片相关字段:标识、标志、片偏移

当 IP 包要经过一条最大传输单元(MTU)比较小的链路时,如果包太大,路由器就得把它切成几片分别发送,到了目的地再拼起来。这三个字段就是为分片服务的。

标识字段是发送方给每个包分配的一个编号,同一个原始包切出来的所有分片,标识字段值相同,接收方靠它判断哪些片该拼在一起。标志字段里有两个有用的位:DF 位为 1 表示"不许分片",如果路上遇到 MTU 不够就直接丢弃并回一个 ICMP 差错报文;MF 位为 1 表示"后面还有分片",为 0 表示这是最后一片。片偏移记录这一片的数据在原始包数据部分中的位置,单位是 8 字节。

这里有个实操细节:因为片偏移以 8 字节为单位,所以每个分片的数据部分长度(除了最后一片)必须是 8 的倍数。这也是为什么分片大小总是 8 的整数倍。我在排查一个视频流卡顿问题时,就是通过抓包看到大量分片且片偏移不连续,才定位到中间某条链路 MTU 配置有问题。

3.4 生存时间与协议号:排查问题的两把钥匙

生存时间(TTL)这个字段特别有意思。它的本意是防止包在网络里无限循环,每过一个路由器减 1,减到 0 就丢弃并回一个 ICMP 超时报文。但实际排查中,它变成了一个非常好用的工具:通过观察 TTL 的初始值和到达时的值,可以大致推断数据经过了多少跳。常见的初始值有 64、128、255,如果你收到一个 TTL 为 57 的包,初始是 64 的话,说明经过了 7 跳。

协议号则是判断上层是什么协议的直接依据。除了前面说的 6(TCP)、17(UDP)、1(ICMP),还有 2(IGMP)、47(GRE)、50(ESP)等等。抓包时看到协议号不认识,直接查一下就能知道这个包承载的是什么。我习惯在写解析脚本时把常见协议号做成一个映射表,读起来一目了然。

4. TCP 报文格式与可靠性机制

4.1 TCP 头部整体布局

TCP 头部标准长度也是 20 字节,带选项时最长 60 字节。它比 IP 头复杂的地方在于,多了很多和"可靠传输"相关的字段。下面这张表是核心字段速查。

字段名位宽作用说明
源端口16 位发送方端口号
目的端口16 位接收方端口号
序列号32 位本报文段数据第一个字节的编号
确认号32 位期望收到的下一个字节编号
数据偏移4 位以 4 字节为单位,表示 TCP 头长度
保留6 位保留未用,一般置 0
控制位6 位URG、ACK、PSH、RST、SYN、FIN
窗口16 位接收方还能接收多少字节
校验和16 位校验首部和数据
紧急指针16 位配合 URG 位使用
选项可变如 MSS、窗口扩大、时间戳等

TCP 之所以复杂,是因为它要在不可靠的 IP 层之上,自己实现一套可靠传输机制。序列号、确认号、窗口、控制位,全都是为这个目标服务的。

4.2 序列号与确认号:可靠传输的核心

TCP 把要发送的数据看成一条连续的字节流,每个字节都有一个编号,这个编号就是序列号。发送方在报文里填上本报文段第一个字节的序列号,接收方收到后,根据数据长度就能算出下一个期望的字节编号,填在确认号里回给对方。

举个例子:发送方发了一个序列号为 1000、数据长度 500 的段,接收方正确收到后,回一个确认号为 1500 的 ACK,意思是"1000 到 1499 我都收到了,请给我 1500 开始的数据"。如果中间丢了,接收方还是回 1000,发送方就知道要重传。这套机制看起来简单,但它是所有可靠传输的基础。

提示:序列号和确认号都是 32 位无符号整数,会回绕。当超过 2 的 32 次方时会从 0 重新开始。做长连接分析时要注意这个回绕问题,否则算出来的差值会突然变成负数。

4.3 六个控制位:连接状态的开关

TCP 头里的 6 个控制位,每一个都对应一种连接状态的变化,是读 TCP 报文时最需要关注的部分。

  • URG:紧急指针有效,表示有紧急数据要优先处理,实际很少用。
  • ACK:确认号有效。除了第一个 SYN 包,其他所有包的 ACK 位都应该是 1。
  • PSH:提示接收方尽快把数据交给应用层,不要缓存。
  • RST:重置连接。收到这个包通常意味着连接出了问题,比如连了一个没监听的端口。
  • SYN:同步序列号,用于建立连接。三次握手的前两个包都带 SYN。
  • FIN:发送方数据发完了,请求关闭连接。

我排查连接问题时,第一件事就是看控制位的组合。比如看到大量 RST,基本可以判断是连接被拒绝或者被中途重置;看到 SYN 发出去了但没有对应的 SYN+ACK 回来,那就是对端没响应或者被防火墙拦了。

4.4 窗口字段与流量控制

窗口字段是 TCP 做流量控制的关键。接收方在每个 ACK 里告诉发送方:"我这边缓冲区还能装多少字节,你别发太快。"发送方根据这个值调整发送速率,避免把接收方撑爆。

窗口值是 16 位,最大 65535 字节。在高带宽高延迟的网络里,这个值可能不够用,所以有了"窗口扩大选项",通过选项字段把窗口按比例放大。抓包时如果看到窗口值一直是 0,说明接收方缓冲区满了,发送方必须停下来等,这就是所谓的"零窗口"。

4.5 选项字段:MSS、窗口扩大、时间戳

TCP 选项字段是可变的,最常见的三个是:

MSS(最大报文段长度):只在 SYN 包里出现,告诉对方"我这边能接收的最大 TCP 段数据部分是多少"。通常是根据本机 MTU 减去 IP 头和 TCP 头算出来的,以太网环境下一般是 1460。

窗口扩大因子:把窗口字段左移若干位,突破 65535 的限制。双方在握手时协商,之后所有窗口值都要按这个因子放大。

时间戳:用于更精确地计算往返时间,以及防止序列号回绕带来的旧包干扰。在高性能网络里几乎是标配。

这三个选项我在写解析脚本时都会专门处理,因为它们的长度和对齐方式不一样,不处理的话后面字段全错位。

5. UDP 与 ICMP 报文格式解析

5.1 UDP 头部:极简主义的代表

UDP 头部只有 8 字节,是三种协议里最简单的。它只有四个字段:源端口、目的端口、长度、校验和,每个都是 16 位。

字段名位宽作用说明
源端口16 位发送方端口号,可选
目的端口16 位接收方端口号
长度16 位UDP 头加数据的总长度,最小为 8
校验和16 位校验首部和数据,可选

UDP 的设计哲学是"我只负责把数据扔出去,可靠性交给上层"。所以它没有序列号、没有确认、没有窗口、没有连接状态。长度字段是 16 位,意味着单个 UDP 数据报最大 65535 字节,减去 8 字节头部,数据部分最多 65527 字节。

注意:UDP 校验和是唯一一个"可选"的校验和字段。如果发送方不算校验和,这个字段填全 0。接收方看到全 0 就跳过校验。但在 IPv6 里,UDP 校验和是强制的,不能省略。

5.2 UDP 的适用场景与常见误区

很多人觉得 UDP 不可靠就一无是处,这是误解。UDP 的优势在于开销小、延迟低、没有连接建立和拆除的成本。DNS 查询、视频直播、在线游戏、VoIP 这些场景,要么数据量小到不值得建连接,要么对延迟极度敏感、丢一两个包无所谓,UDP 反而是更好的选择。

我见过一个误区是"UDP 一定比 TCP 快"。其实在数据量小的时候,UDP 确实省了握手和挥手的时间;但在需要可靠传输的场景,你在应用层自己实现重传和排序,未必比 TCP 做得好。选哪个,取决于你的业务能不能容忍丢包,以及你愿不愿意自己处理可靠性。

5.3 ICMP 报文:网络诊断的探针

ICMP 是 IP 层的"辅助协议",用来传递差错和控制信息。它没有端口号,直接封装在 IP 包里(协议号 1)。ICMP 头部是 8 字节:类型(8 位)、代码(8 位)、校验和(16 位),后面 4 字节根据类型不同含义不同。

常见的类型有:类型 8 是回显请求(ping 请求),类型 0 是回显应答,类型 3 是目的不可达,类型 11 是超时。ping 命令就是发一个类型 8 的 ICMP 包,对方回一个类型 0 的包,通过时间差算往返延迟。

排查网络连通性时,ICMP 是最先用的工具。但要注意,很多设备会屏蔽 ICMP,所以 ping 不通不代表网络不通,可能只是对方不响应 ICMP 而已。这个坑我在排查一个"服务器 ping 不通但服务正常"的问题时踩过,最后发现是对方防火墙策略屏蔽了 ICMP。

6. 实操:用抓包数据逐字段验证

6.1 抓一个 TCP 握手包来读

光看表格记不住,最好的办法是抓一个真实的包对着读。你可以用常见的抓包工具抓一次 TCP 三次握手,然后按下面的顺序逐字段验证。

第一步,找到第一个 SYN 包。看 IP 头:版本应该是 4,首部长度是 5(20 字节),协议号是 6(TCP),总长度一般在 40 到 60 之间。再看 TCP 头:源端口是你发起连接时系统分配的随机端口,目的端口是服务端口,控制位里只有 SYN 是 1,序列号是一个随机初始值。

第二步,找到 SYN+ACK 包。这是服务器回的,控制位里 SYN 和 ACK 都是 1,确认号是"客户端序列号加 1",序列号是服务器自己的随机初始值。

第三步,找到最后的 ACK 包。控制位里只有 ACK 是 1,确认号是"服务器序列号加 1"。到这里连接建立完成。

我建议你把这三个包的序列号和确认号抄下来,自己算一遍加减关系。算通一次,你就彻底理解三次握手了,比看十遍图都管用。

6.2 用脚本解析 IP 头

如果你要写解析代码,下面这段 Python 示例展示了如何按大端序读取 IP 头的关键字段。注意我用的是标准库的 struct 模块,格式字符串里的!表示大端序。

import struct def parse_ip_header(data): # 前 20 字节是 IPv4 固定头部 # !BBHHHBBH4s4s 对应各字段的位宽和类型 (ver_ihl, tos, total_len, ident, flags_frag, ttl, proto, checksum, src, dst) = struct.unpack('!BBHHHBBH4s4s', data[:20]) version = ver_ihl >> 4 # 高 4 位是版本 ihl = (ver_ihl & 0x0F) * 4 # 低 4 位是首部长度,单位 4 字节 return { 'version': version, 'header_len': ihl, 'total_len': total_len, 'ttl': ttl, 'protocol': proto, 'src': '.'.join(map(str, src)), 'dst': '.'.join(map(str, dst)), }

这段代码里最关键的是ver_ihl这个字节的处理:它把版本和首部长度塞在同一个字节里,高 4 位是版本,低 4 位是首部长度。读的时候必须用位运算拆开,这是新手最容易出错的地方。我第一次写的时候直接把整个字节当版本读,结果读出来是 69,一脸懵。

6.3 参数选择与校验和验证

解析完字段后,一个很好的自检方式是验证校验和。IP 头的校验和算法是:把首部按 16 位分组,逐组相加,如果有进位就回卷加到低位,最后取反。如果算出来是 0,说明首部没被破坏。

这个算法看起来绕,但实现起来不难。它的意义在于:如果你解析出来的字段值看起来合理,但校验和验证不通过,那说明要么你的解析偏移错了,要么数据在传输中被破坏了。我在写解析工具时,都会把校验和验证作为一个必做的自检步骤,能挡掉一大半低级错误。

7. 常见问题与排查技巧实录

7.1 字段读错位的典型原因

读报文最常见的错误就是偏移算错。原因通常有三个:一是忘了首部长度字段的单位是 4 字节,直接拿字段值当字节数用;二是忘了选项字段的存在,固定按 20 字节读,结果带选项的包全错位;三是字节序搞反,端口号、序列号读出来是天文数字。

排查这类问题的思路很简单:先打印出你解析的每个字段值,对照抓包工具里显示的字段值。哪个对不上,就从那个字段往前找,看是哪一步偏移算错了。抓包工具是"标准答案",你的解析结果和它不一致,一定是你的问题。

7.2 分片与粘包的区分

分片是 IP 层的行为,一个 IP 包太大被切成多个 IP 包,每个包都有自己的 IP 头,靠标识、标志、片偏移来重组。粘包是 TCP 层的现象,多个应用层消息被合并到一个 TCP 段里发送,或者一个消息被拆到多个段里,接收方需要靠应用层协议自己划分边界。

这两个概念经常被混为一谈。区分方法很简单:看 IP 头。如果多个包的标识字段相同、片偏移不同,那就是分片;如果每个包都是完整的 IP 包、标识各不相同,但 TCP 数据里包含多个消息,那就是粘包。分片在 IP 层解决,粘包在应用层解决,处理层次完全不同。

7.3 常见问题速查表

现象可能原因排查方向
端口号读出来是天文数字字节序搞反检查是否按大端序解析
解析字段全部错位首部长度单位算错确认是否乘以 4
带选项的包解析失败固定按 20 字节读动态读取首部长度
校验和不通过数据损坏或偏移错误先验证偏移再怀疑数据
大量 RST 包连接被拒绝或重置检查端口监听和防火墙
窗口值一直为 0接收方缓冲区满检查接收方处理速度
ping 不通但服务正常ICMP 被屏蔽换用 TCP 探测方式

这张表是我这些年排查问题的经验浓缩,建议收藏。遇到问题先对号入座,能省不少时间。

7.4 几个独家避坑心得

第一个心得:永远不要相信"固定长度"。IP 头和 TCP 头都有可变部分,写解析代码时一定要动态读取长度字段,而不是硬编码 20 字节。我早期写的一个解析脚本就是因为硬编码,遇到带时间戳选项的包就全乱了,排查了半天才发现问题。

第二个心得:抓包时尽量抓全,不要只抓头部。很多问题出在数据部分,比如应用层协议格式错误、数据被截断等。抓包工具默认可能只抓前几十字节,记得把抓取长度调大。

第三个心得:善用抓包工具的过滤功能。一个繁忙的网卡上每秒可能几万个包,不过滤根本没法看。常用的过滤条件比如按端口、按协议、按 IP 过滤,能帮你快速定位到关心的流量。我习惯先按端口过滤,再按控制位过滤,层层缩小范围。

第四个心得:序列号分析要画时间线。单看一个包的序列号没意义,要把一段时间内的包按时间排序,画出序列号和确认号的变化曲线,才能看出重传、乱序、丢包这些现象。我处理过一个吞吐量上不去的问题,就是通过画序列号曲线,发现发送方频繁触发重传,最终定位到是链路质量导致的丢包。

报文格式这东西,看一遍记不住很正常,关键是要动手。抓几个真实的包,对着字段一个一个读,读通几个之后,剩下的就是查表的事了。我到现在也不敢说把所有协议的字段都背下来了,但我知道遇到不认识的字段该去哪查、该怎么验证,这就够了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询