4. DoIP协议栈分层架构

好,咱们今天来聊聊DoIP报文的结构。说实话,搞懂报文头,你就能看懂一半的DoIP通信了。我刚开始接触DoIP的时候,看着那一串十六进制数据,头都大了。后来发现,其实结构非常规整,说白了就是“固定头 + 可变负载”。

4.1 通用DoIP报文头

每个DoIP报文,不管你是发诊断请求还是做路由激活,前面都顶着8个字节的固定头。这8个字节,就是DoIP的“身份证”。

字段 长度(字节) 说明
协议版本 1 当前DoIP协议版本号,ISO 13400-2:2019中固定为0x02
反向协议版本 1 接收方回复时使用的版本,用于版本协商
负载类型 2 标识报文的具体用途,如诊断请求、路由激活等
负载长度 4 从负载类型之后开始计算的字节数,即Payload的长度

嗯,这里要注意。协议版本字段,目前主流用的是0x02。我记得有一次在项目里,供应商的ECU固件版本比较老,发的是0x01,结果我们的测试工具死活连不上。排查了半天,才发现是版本不匹配。所以,协议版本一定要对齐。

反向协议版本这个字段,平时你可能不太关注。它的作用就是让接收方告诉你:“嘿,我支持这个版本”。如果接收方不支持你发的版本,它会在这个字段里填上它支持的版本号,然后给你回一个NACK。

负载类型占2个字节,这是DoIP报文里最核心的字段。后面我会详细展开。

负载长度占4个字节,它告诉接收方:“后面还有多少数据要读”。这个值不包括前面的8字节头,只算Payload的长度。你想想看,如果没有这个长度字段,接收方怎么知道报文什么时候结束?

核心要点:DoIP报文头固定8字节,顺序是:版本(1) + 反向版本(1) + 类型(2) + 长度(4)。解析报文时,先读这8个字节,再根据长度字段读取后续数据。

4.2 DoIP负载类型详解

负载类型(Payload Type),用十六进制表示,范围从0x0000到0xFFFF。ISO 13400标准里定义了几十种类型,但咱们实际项目中常用的,其实就那么几个。

我习惯把负载类型分成三大类:通用管理类、诊断通信类、事件通知类。

4.2.1 通用管理类

这类报文负责DoIP连接的建立和维护。就像你打电话,先得拨号、确认对方是谁,才能开始说话。

负载类型值 名称 方向 用途
0x0000 Generic DoIP header NACK 双向 通用否定应答,用于报告报文头错误
0x0001 Vehicle Identification Request 客户端→车辆 广播请求,询问车辆信息
0x0002 Vehicle Identification Request with EID 客户端→车辆 带逻辑地址的车辆识别请求
0x0003 Vehicle Identification Request with VIN 客户端→车辆 带VIN的车辆识别请求
0x0004 Vehicle Announcement/Identification Response 车辆→客户端 车辆声明或识别响应
0x0005 Routing Activation Request 客户端→车辆 路由激活请求,建立逻辑连接
0x0006 Routing Activation Response 车辆→客户端 路由激活响应

这里我要特别提一下0x0005——路由激活请求。这是DoIP通信的“敲门砖”。你发诊断请求之前,必须先发这个。我曾经见过一个同事,调试的时候忘了发路由激活,结果诊断请求石沉大海,他还在那怀疑网线坏了。

4.2.2 诊断通信类

这类报文才是咱们真正干活的工具。诊断请求和响应,都走这里。

负载类型值 名称 方向 用途
0x8001 Diagnostic Message 双向 UDS诊断请求/响应报文
0x8002 Diagnostic Message Positive Acknowledgement 车辆→客户端 诊断报文肯定应答(ACK)
0x8003 Diagnostic Message Negative Acknowledgement 车辆→客户端 诊断报文否定应答(NACK)

0x8001是诊断报文,它里面装的就是UDS数据。比如你发个10 03(诊断会话控制),外面套上DoIP头,负载类型就是0x8001。

0x8002和0x8003是传输层的确认机制。注意,这不是UDS的肯定/否定响应,而是DoIP层告诉你:“我收到你的诊断报文了,但还没处理”。这个机制在高速通信时特别有用。我个人习惯在写测试脚本时,先检查0x8002/0x8003,再检查UDS响应,这样能更早发现丢包问题。

4.2.3 事件通知类

这类报文是车辆主动发起的,不需要客户端请求。比如车辆上电、下电,或者诊断仪断开连接时,车辆会主动通知。

负载类型值 名称 方向 用途
0x4001 Alive Check Request 车辆→客户端 车辆主动询问客户端是否在线
0x4002 Alive Check Response 客户端→车辆 客户端回复在线确认
0x4003 DoIP Entity Status Request/Response 双向 查询/回复DoIP实体状态
0x4004 Diagnostic Power Mode Information Request/Response 双向 查询/回复诊断电源模式

Alive Check(0x4001/0x4002)这个机制,说白了就是“心跳”。车辆会定期发Alive Check Request,如果你不回复,它就觉得你掉线了,然后自动释放路由。我曾经在测试中遇到过,因为测试脚本处理太慢,没来得及回Alive Check,结果路由被释放了,诊断会话也断了。后来我就在脚本里加了个定时器,专门处理这个。

实战建议:在开发DoIP客户端时,一定要实现Alive Check的自动回复机制。否则,你可能会莫名其妙地发现诊断会话中断,还找不到原因。

4.3 DoIP报文结构总览

为了让你更直观地理解,我画了一张图。这张图展示了DoIP报文的完整结构,从以太网帧头到DoIP负载。

DoIP报文结构总览 以太网帧头(Ethernet Header) 目标MAC(6B) | 源MAC(6B) | EtherType(2B,0x0800 或 0x86DD) IP/UDP/TCP 传输层头 IP头(20B) | UDP头(8B)或TCP头(20B) | 源端口/目标端口 DoIP通用报文头(8字节) 协议版本(1B) | 反向版本(1B) | 负载类型(2B) | 负载长度(4B) 示例:02 FD 80 01 00 00 00 05 DoIP负载(Payload) 长度由“负载长度”字段指定 示例(诊断请求):10 03 00 00 00 00 00 00 解析DoIP报文时,先读8字节头,根据负载类型判断后续数据格式,再根据负载长度读取完整Payload

从这张图可以看得很清楚。DoIP报文是层层嵌套的。最外面是以太网帧,里面是IP/UDP/TCP头,再里面才是DoIP头,最后是负载数据。

4.4 实战:解析一个DoIP报文

光说不练假把式。咱们来手撕一个真实的DoIP报文。

假设我在Wireshark里抓到这样一串数据:

02 FD 80 01 00 00 00 05 10 03 00 00 00 00 00 00

咱们来逐字节解析:

  1. 02:协议版本,ISO 13400-2:2019,没问题。
  2. FD:反向协议版本,对方也支持0x02(0xFD是0x02的按位取反,标准规定这样表示版本协商成功)。
  3. 80 01:负载类型,0x8001,诊断报文。
  4. 00 00 00 05:负载长度,5字节。
  5. 10 03 00 00 00 00 00 00:这是UDS诊断请求。10是服务ID(诊断会话控制),03是子功能(扩展诊断会话)。后面5个字节是填充数据,实际有效载荷就是前2个字节。

你看,是不是很简单?

注意:负载长度字段的值,不包括DoIP头本身的8字节。它只告诉你从负载类型之后开始算,还有多少字节要读。如果你把DoIP头也算进去,解析就会错位。

4.5 常见负载类型速查表

为了方便你日常开发,我把最常用的负载类型整理成了一张速查表。建议你收藏一下,写代码的时候直接查。

类型值 名称 优先级 使用频率
0x0000 Generic DoIP header NACK 高 中
0x0004 Vehicle Announcement/Identification Response 高 高
0x0005 Routing Activation Request 高 高
0x0006 Routing Activation Response 高 高
0x8001 Diagnostic Message 中 极高
0x8002 Diagnostic Message Positive Acknowledgement 中 高
0x8003 Diagnostic Message Negative Acknowledgement 中 中
0x4001 Alive Check Request 低 中
0x4002 Alive Check Response 低 中

我个人习惯把0x8001、0x0005、0x0006这三个类型记在脑子里。因为90%的DoIP通信,都绕不开它们。其他类型,用到的时候查表就行。

小技巧:在写DoIP解析代码时,建议用switch-case处理负载类型。把0x8001、0x0005、0x0006放在最前面,因为它们的命中率最高。这样可以减少分支判断的开销。