第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字节头部 + 负载数据,头部里最关键的是负载类型和负载长度。下一章咱们会讲路由激活,那是建立诊断会话的第一步。