第3章:DoIP协议报文结构
好,咱们今天来聊聊DoIP协议报文结构。说实话,这是整个DoIP诊断的基础。你想想看,如果连报文长什么样都不清楚,那后面的故障码读取、清除根本无从谈起。
我个人习惯把DoIP报文比作一个快递包裹。有外包装,有里面的货物,还有快递单号。咱们今天就把这个包裹拆开,看看里面到底装了啥。
3.1 通用DoIP报文头
每个DoIP报文都有一个固定的头部,8个字节,一个不多一个不少。我刚开始接触DoIP时,总觉得这8个字节记不住,后来发现其实有规律可循。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| 协议版本号 | 1 | 当前固定为0x02(ISO 13400-2:2012) |
| 反向协议版本号 | 1 | 取反,0xFD |
| 负载类型 | 2 | 标识报文用途,后面细讲 |
| 负载长度 | 4 | 后面跟着的数据长度,单位字节 |
嗯,这里要注意。协议版本号和反向协议版本号是一对校验。我曾经在项目里遇到过一台ECU,它只认0x02版本,你发0x03它直接不理你。所以别乱改这个字段。
核心要点:DoIP报文头8字节,前2字节是版本校验,中间2字节告诉你是啥报文,最后4字节告诉你数据有多长。
3.2 负载类型(Payload Type)详解
负载类型是DoIP报文的核心。说白了,就是告诉接收方:我这包数据是干嘛用的。ISO 13400-2里定义了很多类型,但咱们做诊断常用的就那么几个。
| 负载类型值(十六进制) | 名称 | 用途 |
|---|---|---|
| 0x0001 | Generic DoIP header negative acknowledge | 通用否定应答 |
| 0x0002 | Vehicle Identification Request | 车辆识别请求 |
| 0x0004 | Vehicle Identification Response | 车辆识别响应 |
| 0x0005 | Routing Activation Request | 路由激活请求 |
| 0x0006 | Routing Activation Response | 路由激活响应 |
| 0x8001 | Diagnostic Message | 诊断消息(UDS报文) |
你看,0x8001这个值特别重要。咱们后面要讲的故障码读取和清除,用的就是它。我记得有一次调试,死活收不到诊断响应,后来发现负载类型写成了0x0001,那当然不行——那是否定应答的标识。
小技巧:如果你用Wireshark抓包,可以直接看到负载类型的字符串描述。但自己写代码时,一定要用数值判断,别用字符串匹配,效率差太多了。
3.3 DoIP报文封装与解析
好,理论讲完了,咱们来点实际的。怎么把一个UDS诊断请求封装成DoIP报文?又怎么从收到的DoIP报文中把UDS响应解析出来?
我直接上代码吧。这是我自己项目里用过的Python代码,稍微简化了一下。
import struct
def pack_doip_message(payload_type, payload_data):
"""
封装DoIP报文
:param payload_type: 负载类型,如0x8001
:param payload_data: 负载数据,bytes类型
:return: 完整的DoIP报文
"""
# 协议版本号
protocol_version = 0x02
# 反向协议版本号
inverse_protocol_version = 0xFD
# 负载长度
payload_length = len(payload_data)
# 使用struct打包,大端序
header = struct.pack('!BBHI',
protocol_version,
inverse_protocol_version,
payload_type,
payload_length)
return header + payload_data
def unpack_doip_message(data):
"""
解析DoIP报文
:param data: 接收到的原始数据
:return: (负载类型, 负载数据)
"""
# 先解析头部
protocol_version, inverse_version, payload_type, payload_length = \
struct.unpack('!BBHI', data[:8])
# 校验版本号
if protocol_version != 0x02:
print(f"警告:协议版本不匹配,期望0x02,收到{hex(protocol_version)}")
# 提取负载数据
payload_data = data[8:8+payload_length]
return payload_type, payload_data
# 示例:封装一个读取故障码的UDS请求
# UDS 0x19 0x02 0xFF 表示读取所有故障码
uds_request = bytes([0x19, 0x02, 0xFF])
doip_packet = pack_doip_message(0x8001, uds_request)
print(f"封装的DoIP报文(十六进制):{doip_packet.hex()}")
# 示例:解析收到的DoIP报文
# 假设这是收到的响应
received_data = doip_packet # 实际场景中是从socket接收的
payload_type, payload = unpack_doip_message(received_data)
print(f"负载类型:{hex(payload_type)}")
print(f"UDS响应数据:{payload.hex()}")
避坑指南:我曾经在解析时犯过一个低级错误——忘记校验负载长度。结果对方发了个超长报文,我直接内存溢出了。所以,解析时一定要检查payload_length是否在合理范围内,比如诊断报文一般不超过4096字节。
你可能会问:为什么用struct.pack('!BBHI', ...)?这里的!表示网络字节序(大端序),B是无符号字节,H是无符号短整型(2字节),I是无符号整型(4字节)。这个顺序和DoIP报文头完全对应。
嗯,这里再补充一点。实际项目中,你收到的可能不是完整的一包数据。TCP是流式传输,可能粘包,也可能分包。所以解析时一定要先收够8字节头部,再根据payload_length收完剩余数据。我见过不少新手在这里栽跟头。
实战建议:写一个DoIP接收缓冲区,每次收到数据先缓存,然后循环解析。解析完一包就移除,直到缓存区不够8字节或数据不完整。这样能优雅处理粘包问题。
好了,这一章的内容就到这里。DoIP报文结构其实不复杂,但细节很多。你只要记住:8字节头部 + 负载数据,头部里最关键的是负载类型和负载长度。下一章咱们会讲路由激活,那是建立诊断会话的第一步。