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报文是层层嵌套的。最外面是以太网帧,里面是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
咱们来逐字节解析:
- 02:协议版本,ISO 13400-2:2019,没问题。
- FD:反向协议版本,对方也支持0x02(0xFD是0x02的按位取反,标准规定这样表示版本协商成功)。
- 80 01:负载类型,0x8001,诊断报文。
- 00 00 00 05:负载长度,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放在最前面,因为它们的命中率最高。这样可以减少分支判断的开销。