2、DoIP协议栈架构:OSI模型映射、DoIP实体与网关的概念、报文结构
好,咱们进入正题。这一节我打算把DoIP的协议栈架构彻底讲透。你可能会问,学这个有什么用?我举个例子——有一次我在现场排查一台车连不上诊断仪,折腾了半天,最后发现是DoIP实体和网关的配置搞混了。从那以后,我就特别重视这部分基础。
2.1 OSI模型映射:DoIP到底在哪个层级干活?
先看一张经典的OSI七层模型。DoIP不是凭空冒出来的,它老老实实地坐在传输层和应用层之间。我个人习惯把DoIP理解为「诊断数据的快递员」——它负责把UDS诊断请求打包,然后通过TCP/IP网络送出去。
| OSI层 | DoIP对应协议/功能 | 我的一点理解 |
|---|---|---|
| 应用层(7) | UDS诊断服务(ISO 14229) | 真正干活的,比如读故障码 |
| 表示层(6) | DoIP报文封装/解封装 | 把UDS数据包上DoIP头部 |
| 会话层(5) | DoIP连接管理、路由激活 | 谁连上来了?权限够不够? |
| 传输层(4) | TCP(可靠通信)、UDP(发现) | TCP传数据,UDP做广播 |
| 网络层(3) | IPv4/IPv6 | IP地址、路由 |
| 数据链路层(2) | 以太网MAC | 物理地址,帧格式 |
| 物理层(1) | 100BASE-TX / 1000BASE-T | 网线、接口 |
嗯,这里要注意:DoIP主要工作在会话层和表示层。传输层以下的事,它基本不管,交给操作系统协议栈就行。我在项目里见过有人非要在DoIP层做重传机制,其实没必要——TCP已经帮你干好了。
核心要点:DoIP = UDS诊断数据 + TCP/IP网络传输。它不负责「诊断什么」,只负责「怎么传过去」。
2.2 DoIP实体(Entity)与网关(Gateway)的概念
这两个概念特别容易搞混。我刚开始接触DoIP时,以为实体就是网关,网关就是实体。后来被老工程师骂了一顿才明白——完全两码事。
2.2.1 DoIP实体(Entity)
说白了,DoIP实体就是「能说话的那个节点」。它具备完整的DoIP协议栈,能接收诊断请求、发送响应。一辆车上可以有多个DoIP实体,比如:
- 主控ECU:直接连接以太网,自己就是DoIP实体
- 网关ECU:如果它实现了DoIP协议栈,那它也是实体
- 远程信息处理单元(T-Box):经常充当DoIP实体
每个DoIP实体都有一个唯一的逻辑地址(Logical Address),就像人的身份证号。我记得有一次调试,发现两个ECU的逻辑地址冲突了,结果诊断仪死活连不上其中一个。查了半天,原来是配置表写错了。
小技巧:逻辑地址在DoIP报文的源地址和目标地址字段里体现。建议在项目初期就做好地址分配表,避免后期打架。
2.2.2 DoIP网关(Gateway)
网关的角色更像「翻译官」或「路由器」。它连接着以太网和车内其他网络(CAN、LIN、FlexRay)。外部诊断仪通过以太网发来DoIP报文,网关负责:
- 解析DoIP头部,取出UDS数据
- 把UDS数据转换成CAN报文(或其他总线格式)
- 转发给目标ECU
- 把响应再封装回DoIP报文,发回诊断仪
你想想看,如果车内全是CAN节点,没有以太网能力,那网关就是唯一的DoIP实体。它既要处理自己的诊断,还要帮别人传话。我曾经在一个项目里,网关同时要转发20多个CAN节点的诊断数据,CPU负载直接飙到80%。后来优化了报文调度才压下来。
避坑指南:我曾经遇到过网关的DoIP缓冲区设置太小,导致并发诊断请求时丢包。建议把接收缓冲区至少设到64KB,别省这点内存。
2.3 DoIP报文结构:通用头部详解
好,到了最硬核的部分。DoIP报文结构其实不复杂,但细节很多。我习惯把通用头部叫做「信封」——它告诉接收方这封信是谁写的、写给谁、有多长、是什么类型。
2.3.1 通用头部格式
所有DoIP报文(不管是TCP还是UDP)都包含这个8字节的通用头部:
字节偏移 | 字段名 | 长度 | 描述
---------|-----------------|------|-------------------------------
0 | 协议版本 | 1 | 当前固定为0x02(ISO 13400-2)
1 | 反向协议版本 | 1 | 同样为0x02
2-3 | 负载类型 | 2 | 标识报文用途(见下表)
4-7 | 负载长度 | 4 | 后续数据的字节数(不含头部)
嗯,这里有个坑:负载长度字段是4字节,但实际项目中很少超过65535字节。不过标准留了余量,咱们也别自作聪明去截断它。
2.3.2 常见的负载类型(Payload Type)
我整理了一份常用列表,你在实际抓包时肯定用得上:
| 负载类型值(十六进制) | 名称 | 用途 |
|---|---|---|
| 0x0001 | Generic DoIP header negative acknowledge | 通用否定应答(比如版本不匹配) |
| 0x0002 | Vehicle Identification Request | 车辆识别请求(UDP广播) |
| 0x0004 | Vehicle Identification Response | 车辆识别响应 |
| 0x0005 | Routing Activation Request | 路由激活请求(TCP连接后第一步) |
| 0x0006 | Routing Activation Response | 路由激活响应 |
| 0x8001 | Diagnostic Message | 真正的UDS诊断数据(TCP) |
为什么会有0x0001这种否定应答?我遇到过一种情况:诊断仪用旧版协议(0x01)发请求,但车端只支持新版(0x02)。这时候车端就会回复0x0001,告诉对方「版本不对,请升级」。说白了,这就是协议层面的握手检查。
2.3.3 一个完整的DoIP报文示例
假设诊断仪要发送一个UDS诊断请求(比如读取VIN码),完整的DoIP报文长这样:
02 02 80 01 00 00 00 0A // 通用头部:版本0x02,负载类型0x8001,长度10字节
FD 00 00 0E 00 00 00 00 // DoIP诊断消息头部:源地址0xFD00,目标地址0x000E
22 F1 90 // UDS请求:读取VIN(22服务,F190标识符)
我来拆解一下:
- 02 02:协议版本和反向版本,都是0x02
- 80 01:负载类型,表示这是诊断消息
- 00 00 00 0A:负载长度,10字节(后面所有数据的总和)
- FD 00:源逻辑地址,诊断仪的地址
- 00 0E:目标逻辑地址,目标ECU的地址
- 22 F1 90:UDS请求体
注意:DoIP诊断消息头部(源地址+目标地址+用户数据)是包含在负载长度里的。也就是说,负载长度 = 4字节(源+目标) + N字节(UDS数据)。这个细节我见过不少人算错。
2.4 小结与个人经验
这一节内容不少,我帮你捋一下重点:
- DoIP在OSI模型中跨会话层和表示层,底层依赖TCP/UDP
- DoIP实体是「能独立处理DoIP协议的节点」,网关是「连接不同总线的翻译官」
- 通用头部8字节,固定格式,负载类型决定了报文用途
- 诊断消息(0x8001)是实际传输UDS数据的载体
最后说句掏心窝的话:DoIP协议栈看着条条框框很多,但只要你亲手抓一次包、拆一次报文,立马就通了。我建议你找个开源DoIP实现(比如Python的python-doenet),在本地搭个虚拟环境跑一跑。纸上得来终觉浅,绝知此事要躬行。
下一节咱们聊「车辆发现机制」,也就是诊断仪怎么在网络上找到车。那个过程很有意思,有点像「网络版的捉迷藏」。