3、报文结构解析:以太网帧、IP头、TCP/UDP头、应用层载荷,逐层拆解

各位好,我是老周。今天咱们来聊聊报文结构。说实话,做嵌入式通信这十几年,我见过太多同事拿着抓包文件两眼一抹黑——明明数据就在那儿,就是看不懂。说白了,报文解析就是协议栈调试的“基本功”,就像练武要先扎马步一样。

我个人的习惯是,遇到设备入网失败,第一件事不是翻代码,而是先抓包、再拆包。你想想看,报文在网络上跑,每一层都加了“信封”,我们得一层层剥开,才能看到真正的数据。今天我就带大家把这个过程走一遍。

3.1 以太网帧:物理层的第一道“信封”

报文从网口出来,最先碰到的是以太网帧。嗯,这里要注意,以太网帧不是IP包,它是最底层的“运输容器”。

字段 长度(字节) 说明
前导码 7 同步时钟,实际抓包时通常不显示
帧起始定界符 1 标记帧开始
目的MAC地址 6 接收方网卡地址
源MAC地址 6 发送方网卡地址
类型/长度 2 0x0800表示IPv4,0x0806表示ARP
载荷 46-1500 上层数据(IP包)
帧校验序列 4 CRC32校验

我在项目中遇到过一件事:有个同事调了一整天,设备就是连不上服务器。我过去一看抓包,发现目的MAC地址全是广播地址(FF:FF:FF:FF:FF:FF)。这哪是正常通信?分明是ARP请求没得到回应。所以你看,第一层就能发现问题。

小技巧:抓包时如果看到大量“谁有IP x.x.x.x?告诉y.y.y.y”的ARP包,说明设备在二层找不到网关的MAC地址。先查网线、查交换机端口、查VLAN配置。

3.2 IP头:网络层的“路由标签”

剥开以太网帧,里面就是IP包。IP头才是真正决定数据“去哪儿”的关键。我个人觉得,IP头里最容易被忽略的是“生存时间(TTL)”字段——很多入网失败其实跟它有关。

IP头结构(20字节固定部分):
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|版本| 首部长度 |   服务类型   |         总长度(字节)         |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          标识                |标志|     片偏移                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|  生存时间(TTL)  |   协议     |        首部校验和              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                        源IP地址                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|                      目的IP地址                               |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

这里我重点说几个字段:

  • 协议字段:6表示TCP,17表示UDP。抓包时如果看到协议号不对,那肯定是封装层出问题了。
  • TTL:每经过一个路由器减1,减到0就丢弃。我曾经调试一个跨省入网项目,设备TTL设成了1,结果数据连第一个网关都没出去——直接死在门口了。
  • 首部校验和:只校验IP头本身,不校验载荷。这个校验和错了,路由器直接丢包,而且不会通知发送方。
避坑指南:我曾经遇到过一个案例,设备发送的IP包总长度字段写的是1500,但实际载荷只有100字节。交换机收到后等不到后续数据,直接丢弃。后来发现是协议栈里长度计算函数写错了——把缓冲区大小当成了数据长度。所以,抓包时一定要核对“总长度”和实际数据是否匹配。

3.3 TCP/UDP头:传输层的“端口门牌”

IP头告诉我们数据要去哪台机器,TCP/UDP头则告诉机器把数据交给哪个应用程序。说白了,端口号就是应用程序的“门牌号”。

3.3.1 TCP头(20字节固定部分)

字段 长度(位) 说明
源端口 16 发送方应用程序端口
目的端口 16 接收方应用程序端口
序列号 32 数据流中第一个字节的序号
确认号 32 期望收到的下一个字节序号
数据偏移 4 TCP头长度(单位:4字节)
标志位 9 SYN、ACK、FIN、RST等
窗口大小 16 接收窗口(流量控制)
校验和 16 覆盖TCP头和载荷
紧急指针 16 URG标志置位时有效

你想想看,TCP三次握手的SYN、SYN-ACK、ACK,其实就是靠这些标志位来沟通的。我调试入网协议时,最喜欢看的就是“握手阶段”——如果只看到SYN发出,没有SYN-ACK回来,那八成是服务器端口没开或者防火墙拦了。

3.3.2 UDP头(8字节固定部分)

UDP头结构:
 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|          源端口               |         目的端口              |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|           长度                |         校验和                |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+

UDP比TCP简单得多,没有握手、没有重传、没有流量控制。说白了就是“发出去就不管了”。很多物联网设备用UDP做入网广播,因为快、开销小。但代价是——丢包了也不知道。

关键区别:TCP头有20字节固定部分,UDP头只有8字节。TCP保证可靠传输,UDP不保证。如果你的设备入网协议用的是UDP,一定要在应用层自己做超时重传,否则丢包了设备就“失联”了。

3.4 应用层载荷:真正的“业务数据”

剥完前三层,剩下的就是应用层载荷。这才是设备入网协议真正要传递的数据。比如MQTT的CONNECT报文、CoAP的POST请求、或者自定义的二进制协议。

我见过最离谱的案例:一个同事抓包看到应用层数据全是0x00,但代码里明明写了发送“hello”。查了半天,发现是DMA传输时源地址指针指向了未初始化的内存区域。所以,应用层载荷一定要和协议文档逐字节比对。

// 一个典型的自定义入网请求报文(16进制):
// 帧头(2B) | 长度(1B) | 命令(1B) | 设备ID(4B) | 校验(1B) | 帧尾(2B)
// 0xAA 0x55 | 0x08 | 0x01 | 0x00 0x01 0x02 0x03 | 0xF7 | 0x0D 0x0A

// 解析代码片段:
uint8_t buffer[] = {0xAA, 0x55, 0x08, 0x01, 0x00, 0x01, 0x02, 0x03, 0xF7, 0x0D, 0x0A};

if (buffer[0] == 0xAA && buffer[1] == 0x55) {  // 帧头校验
    uint8_t len = buffer[2];                     // 载荷长度
    uint8_t cmd = buffer[3];                     // 命令字
    uint32_t dev_id = (buffer[4] << 24) | (buffer[5] << 16) | 
                      (buffer[6] << 8) | buffer[7];  // 设备ID
    // 继续解析...
}

3.5 逐层拆解实战:一张图看懂全过程

下面这张图是我自己画的一个报文逐层拆解流程。每次调试入网协议时,我都会在脑子里过一遍这个流程——从网线上的电信号,到应用程序能识别的数据,中间经历了什么。

报文逐层拆解流程图 第1层:物理层(以太网帧) 前导码 | 目的MAC | 源MAC | 类型(0x0800) | 载荷 | FCS 第2层:网络层(IP头) 版本 | 首部长度 | TTL | 协议(6=TCP/17=UDP) | 源IP | 目的IP 第3层:传输层(TCP/UDP头) 源端口 | 目的端口 | 序列号 | 确认号 | 标志位 | 窗口大小 第4层:应用层(业务数据) MQTT/CoAP/HTTP/自定义协议 | 设备ID | 命令字 | 载荷数据 💡 调试要点 抓包时从下往上逐层核对:MAC地址→IP地址→端口号→应用数据

这张图其实就说明了一件事:报文是“洋葱”,得一层层剥。我每次调试入网协议,都会在Wireshark里先看以太网帧的MAC地址对不对,再看IP头的TTL和校验和,然后看TCP/UDP的端口和标志位,最后才看应用层数据。顺序不能乱,乱了就容易漏掉关键线索。

实战建议:在Wireshark里,你可以直接展开每一层查看。我个人习惯先看“Expert Info”选项卡——它会自动标记出异常,比如TCP重传、校验和错误、重复ACK等。这些往往是入网失败的“元凶”。

好了,报文结构就讲到这里。下一层一层拆解的能力,说白了就是“眼力”——你看到的不再是乱码,而是有结构的协议数据。下次抓包时,试着按这个顺序走一遍,你会发现很多问题其实就藏在那些不起眼的字段里。