资讯动态

DoIP协议入门与实战:从UDP车辆发现到TCP诊断通信的完整链路

发布时间:2026/10/22 17:59:44 来源:尧图企业网站定制
简介这份PPT面向汽车电子诊断工程师、车载网络测试人员及智能网联汽车相关专业学生系统讲解DoIPDiagnostics over IP基础知识帮助读者理解基于以太网与IP协议的车辆诊断技术原理与实现路径。内容围绕诊断通信与固件更新两大应用场景展开涵盖TCP/UDP协议分工、CDD与ODX描述文件、诊断测试器与网关的逻辑地址分配、车辆发现与车辆公告、IP地址配置、路由激活请求与响应序列以及DoIP消息的数据包结构等核心模块并配有CANoe/CANalyzer.Ethernet环境下的示例说明。资源包共1个文件为pptx演示文稿整体约5.11MB页面结构按议程分节组织便于按主题查阅与课堂讲解。目前已有110人学习适合作为DoIP入门培训、技术分享或诊断协议自学的参考材料。1. 从一份诊断协议课件说起DoIP 到底解决了什么问题很多刚接触车载以太网诊断的工程师第一次看到ETH_01_DoIP_Basics.pptx这类课件标题时第一反应是「这不就是个 PPT 吗」。但真正上手过整车诊断的人会告诉你DoIPDiagnostics over IP是传统 CAN 诊断迈向以太网诊断绕不开的第一道门槛。它的核心价值很直接把原本跑在 CAN 总线上的 UDS 诊断报文搬到以太网 TCP/IP 协议栈上传输从而获得几十倍甚至上百倍的带宽提升。传统 CAN 诊断刷写一个 ECU 动辄几十分钟而 DoIP 能把同样的刷写压缩到几分钟级别这在整车下线检测和售后诊断里是实打实的效率差异。这份课件标题里的ETH_01暗示它是某个以太网诊断系列的第一讲Basics说明它面向的是入门认知。所以这篇文章不打算复述课件内容而是顺着这个标题把 DoIP 的协议栈结构、最小可跑通的连接流程、关键参数配置和实际踩坑点讲清楚。适合两类人一类是刚被安排做以太网诊断测试、还没搞明白 DoIP 和 UDS 关系的测试工程师另一类是想从 CAN 诊断迁移到以太网诊断、需要一份可复现路径的开发者。读完你应该能自己搭起一个 DoIP 通信的最小验证环境并知道哪些参数设错了会直接导致连接失败。2. DoIP 协议栈拆解从物理层到诊断报文怎么一层层套上去2.1 DoIP 在 OSI 模型里的位置和它依赖的三层协议DoIP 不是一个独立协议它是建立在 TCP/IP 之上的应用层协议。理解它的第一步是搞清楚它依赖哪些底层协议。物理层和链路层通常是 100BASE-TX 或 1000BASE-T1 车载以太网网络层用 IPv4部分场景也支持 IPv6传输层同时用到 TCP 和 UDP。这里有个容易混淆的点DoIP 并不是只用 TCP它把功能分给了两个传输层协议。UDP 负责三件事车辆发现Vehicle Discovery、车辆识别请求与响应、以及路由激活前的状态查询。TCP 负责真正的诊断报文传输包括诊断请求、诊断响应和刷写数据流。为什么这么分因为车辆发现是广播行为不需要可靠连接用 UDP 广播效率最高而诊断报文要求可靠有序传输必须走 TCP。这个分工是 DoIP 设计里最基础也最容易被忽略的一点很多连接失败的问题根源就在于没搞清楚某个报文该走 UDP 还是 TCP。从端口号来看DoIP 有明确的约定UDP 使用 13400 端口做车辆发现和识别TCP 也使用 13400 端口做诊断通信。这个端口号是 ISO 13400 标准规定的不是随便选的。实际配置时如果防火墙或交换机做了端口过滤13400 的 UDP 和 TCP 都要放行只放一个会导致部分功能不可用。2.2 DoIP 报文头的固定结构和各字段含义DoIP 所有报文都有一个 8 字节的固定头部这个结构必须记牢因为解析任何 DoIP 报文都从这 8 个字节开始。结构如下字段字节数含义Protocol Version1协议版本常见 0x02 对应 ISO 13400-2:2012Inverse Protocol Version1版本号取反用于校验Payload Type2报文类型决定后续载荷怎么解析Payload Length4载荷长度不含头部 8 字节Protocol Version 和它的取反值是一对校验关系。比如版本 0x02取反就是 0xFD两个字节相加等于 0xFF。接收方收到报文先校验这两个字节不匹配直接丢弃。这个设计是为了在噪声环境下快速过滤无效报文实际调试时如果抓包看到版本字节不匹配说明发送方配置的协议版本和接收方期望的不一致。Payload Type 是理解 DoIP 功能的关键。常见的类型包括0x0001 车辆识别请求、0x0004 车辆识别响应、0x0005 路由激活请求、0x0006 路由激活响应、0x8001 诊断报文、0x8002 诊断报文肯定响应、0x8003 诊断报文否定响应。记住这几个类型抓包时就能快速判断通信卡在哪一步。2.3 用 Python 构造一个最小 DoIP 车辆识别请求光看结构不够动手构造一个报文才能真正理解。下面用 Python 发一个 UDP 车辆识别请求这是 DoIP 通信的第一步import socket import struct # DoIP 车辆识别请求报文构造 # 头部版本0x02取反0xFD类型0x0001载荷长度0 doip_header struct.pack(BBHI, 0x02, 0xFD, 0x0001, 0x00) payload b # 完整报文 doip_packet doip_header payload # UDP 广播发送到 13400 端口 sock socket.socket(socket.AF_INET, socket.SOCK_DGRAM) sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1) sock.settimeout(3) # 3秒超时等待车辆识别响应 try: sock.sendto(doip_packet, (255.255.255.255, 13400)) data, addr sock.recvfrom(1024) print(f收到来自 {addr} 的响应长度 {len(data)} 字节) # 解析响应头部 ver, inv_ver, ptype, plen struct.unpack(BBHI, data[:8]) print(f版本: {ver:#04x}, 类型: {ptype:#06x}, 载荷长度: {plen}) except socket.timeout: print(超时未收到车辆识别响应) finally: sock.close()这段代码的逻辑很直接构造 8 字节头部载荷为空通过 UDP 广播发到 13400 端口。struct.pack(BBHI, ...)里的表示大端字节序这是网络协议的标准字节序DoIP 也用大端。B是无符号单字节H是无符号双字节I是无符号四字节正好对应头部的字段宽度。参数上最需要注意的是超时时间。车辆识别响应通常很快但如果网络里有多个 DoIP 实体响应可能陆续到达3 秒是个比较稳妥的等待窗口。另外广播地址用255.255.255.255是受限广播如果跨网段可能需要用子网定向广播地址。实际测试时如果一直超时先确认目标设备是否在同一广播域以及 13400 UDP 端口是否可达。3. 路由激活与诊断会话建立从 UDP 发现到 TCP 通信的完整链路3.1 路由激活请求的构造和关键参数车辆识别只是告诉你有这么个设备存在真正要发诊断报文必须先做路由激活Routing Activation。这一步走 TCP目的是让 DoIP 实体知道你要开始诊断通信了并分配一个逻辑地址给你。路由激活请求的载荷结构比车辆识别复杂字段字节数含义激活类型20x0000 默认0x0001 WWH-OBD0xE000 中央安全源地址2诊断仪的逻辑地址通常 0x0E00 起保留4固定 0x00000000可选激活类型4部分场景需要无则省略源地址是这里最关键的参数。DoIP 用逻辑地址区分不同诊断实体诊断仪一般用 0x0E00 到 0x0EFF 这个范围ECU 用 0x0001 到 0x0DFF。如果源地址配错路由激活响应会返回否定码常见的是 0x02源地址不匹配或 0x03源地址已激活。实际项目里每个诊断仪的源地址通常是固定的不能随意改否则会和整车网络里的地址规划冲突。3.2 用 Python 完成 TCP 连接和路由激活下面这段代码在车辆识别之后建立 TCP 连接并发送路由激活请求import socket import struct # 假设已经通过车辆识别拿到了目标 IP target_ip 192.168.1.100 target_port 13400 # 建立 TCP 连接 tcp_sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) tcp_sock.settimeout(5) tcp_sock.connect((target_ip, target_port)) # 构造路由激活请求 # 头部版本0x02取反0xFD类型0x0005载荷长度7 # 载荷激活类型0x0000源地址0x0E00保留4字节0 activation_type 0x0000 source_address 0x0E00 reserved 0x00000000 payload struct.pack(HHI, activation_type, source_address, reserved) doip_header struct.pack(BBHI, 0x02, 0xFD, 0x0005, len(payload)) tcp_sock.send(doip_header payload) # 接收路由激活响应 resp tcp_sock.recv(1024) ver, inv_ver, ptype, plen struct.unpack(BBHI, resp[:8]) print(f响应类型: {ptype:#06x}) if ptype 0x0006: # 解析响应载荷逻辑地址2字节 响应码1字节 保留4字节 tester_addr, resp_code struct.unpack(HB, resp[8:11]) print(f分配的逻辑地址: {tester_addr:#06x}, 响应码: {resp_code:#04x}) if resp_code 0x10: print(路由激活成功可以发送诊断报文) else: print(f路由激活失败响应码 {resp_code:#04x})逻辑上这段代码先建立 TCP 连接然后发送类型 0x0005 的路由激活请求。响应类型 0x0006 表示路由激活响应载荷里的响应码 0x10 表示成功。其他常见响应码0x00 表示拒绝、0x01 表示需要确认、0x02 表示源地址不匹配、0x03 表示源地址已激活、0x04 表示缺少认证。参数说明上activation_type用 0x0000 是默认激活适用于大多数诊断场景。source_address必须和整车地址规划一致这个值通常由诊断规范定义不能自己拍脑袋定。如果响应码返回 0x03说明这个源地址已经被另一个连接占用了需要先断开旧连接或换一个源地址。3.3 诊断报文的封装UDS 怎么塞进 DoIP 载荷路由激活成功后就可以发诊断报文了。DoIP 诊断报文的载荷结构是源地址 2 字节 目标地址 2 字节 UDS 数据。UDS 数据就是标准的 UDS 服务请求比如 0x10 0x03 表示进入扩展会话。# 发送 UDS 诊断请求进入扩展会话0x10 0x03 source_addr 0x0E00 target_addr 0x0001 # 目标 ECU 的逻辑地址 uds_data bytes([0x10, 0x03]) diag_payload struct.pack(HH, source_addr, target_addr) uds_data diag_header struct.pack(BBHI, 0x02, 0xFD, 0x8001, len(diag_payload)) tcp_sock.send(diag_header diag_payload) # 接收诊断响应 diag_resp tcp_sock.recv(1024) ver, inv_ver, ptype, plen struct.unpack(BBHI, diag_resp[:8]) if ptype 0x8002: src, tgt struct.unpack(HH, diag_resp[8:12]) uds_resp diag_resp[12:] print(fUDS 响应: {uds_resp.hex()})这里的关键是目标地址必须正确。每个 ECU 在整车网络里都有唯一的逻辑地址发错了要么没响应要么收到否定响应。实际调试时如果诊断请求发出去没反应先检查目标地址是否和 ECU 实际地址一致再看 TCP 连接是否还活着。4. DoIP 实操避坑那些让连接莫名失败的参数和配置4.1 坑一UDP 广播收不到车辆识别响应现象是发送车辆识别请求后一直超时抓包能看到请求发出去了但没有响应。原因通常有三个一是诊断仪和目标设备不在同一广播域广播包被路由器拦了二是目标设备的 13400 UDP 端口没开或被防火墙过滤三是目标设备配置了静态 IP 但诊断仪用的是 DHCP网段不一致。解决办法分步排查先用ping确认 IP 层可达再用nc -u或类似工具测试 UDP 端口是否开放最后确认广播地址是否正确。如果是跨网段场景需要用定向广播地址而不是 255.255.255.255。我一般会先用 Wireshark 抓包看请求到底有没有到达目标设备这一步能排除一半的问题。4.2 坑二路由激活返回 0x03 源地址已激活这个坑很典型。现象是路由激活响应码返回 0x03连接建立失败。原因是同一个源地址已经有一个活跃的 TCP 连接了DoIP 实体不允许同一源地址重复激活。常见于上一次测试异常退出TCP 连接没正常关闭服务端还认为连接活着。解决方式是先发一个路由激活释放请求或者等 TCP 超时自动断开。更稳妥的做法是在代码里加异常处理确保每次测试结束都主动关闭 TCP 连接。如果频繁遇到这个问题可以在路由激活前先发一个类型 0x0005 但激活类型为 0x0000 的请求探测一下或者直接换一个源地址。4.3 坑三诊断报文发出去收到 0x8003 否定响应现象是路由激活成功了但发诊断请求收到类型 0x8003 的否定响应。原因是目标 ECU 不接受这个诊断请求可能是会话模式不对、安全访问没通过、或者目标地址不是这个 ECU 的地址。否定响应的载荷里会带一个 NRC否定响应码比如 0x7F 0x10 0x13 表示服务 0x10 的参数 0x13 不支持。解决方式是先解析 NRC确认是服务不支持还是条件不满足。如果是安全访问问题需要先走 0x27 服务解锁。如果是会话问题需要先切到正确的会话模式。实际项目里最常见的否定响应是 0x7F 0x10 0x13请求超出范围和 0x7F 0x27 0x35密钥无效前者通常是参数写错后者是安全算法没对上。4.4 坑四TCP 连接建立成功但收不到任何响应现象是 TCP 三次握手成功但发出去的 DoIP 报文石沉大海。原因可能是协议版本不匹配、载荷长度字段写错、或者目标设备期望的报文类型不对。DoIP 实体收到格式错误的报文会直接丢弃不会返回错误响应所以表现就是「没反应」。排查方法是逐字节对照报文结构重点检查版本字节和取反字节是否匹配、Payload Length 是否和实际载荷长度一致。我遇到过最常见的情况是 Payload Length 算错了比如把头部 8 字节也算进去了导致接收方解析错位。用 Wireshark 的 DoIP 解析器能直接看到哪个字段异常比自己数字节快得多。4.5 坑五刷写大文件时 TCP 连接中途断开现象是刷写进行到一半 TCP 连接断了诊断会话丢失。原因是 DoIP 刷写数据量大如果 TCP 窗口设置不合理或网络有丢包容易触发超时断开。另外部分 DoIP 实体有连接空闲超时如果两条诊断报文间隔太长也会被断开。解决办法是调整 TCP keepalive 参数缩短心跳间隔。在代码层面刷写过程中要持续发送诊断报文保持连接活跃不能有太长的空闲。如果网络质量差可以考虑减小每次发送的数据块大小降低单次传输失败的概率。实际刷写时我一般会把 TCP 超时设到 30 秒以上并在刷写循环里加一个保活报文。5. 进阶技巧用 Wireshark 和脚本把 DoIP 调试效率提上去5.1 用 Wireshark 过滤和解析 DoIP 报文Wireshark 自带 DoIP 解析器但默认可能没启用。在「分析」→「启用的协议」里确认 doip 被勾选。过滤表达式用doip就能筛出所有 DoIP 报文进一步可以用doip.payload_type 0x8001只看诊断请求或者doip.source_address 0x0E00按源地址过滤。解析窗口里会直接展开头部字段和载荷内容比手工解析快很多。我习惯把常用的过滤表达式存成按钮调试时一键切换。另外 Wireshark 的「追踪 TCP 流」功能能把一次诊断会话的完整交互串起来看排查时序问题特别有用。5.2 写一个 DoIP 诊断脚本模板把前面几段代码整合成一个可复用的脚本模板能省掉大量重复劳动。核心结构是车辆识别 → TCP 连接 → 路由激活 → 诊断请求 → 解析响应 → 关闭连接。下面是一个精简的模板框架class DoIPClient: def __init__(self, target_ip, source_addr0x0E00): self.target_ip target_ip self.source_addr source_addr self.tcp_sock None def connect(self): self.tcp_sock socket.socket(socket.AF_INET, socket.SOCK_STREAM) self.tcp_sock.settimeout(10) self.tcp_sock.connect((self.target_ip, 13400)) return self._routing_activation() def _routing_activation(self): payload struct.pack(HHI, 0x0000, self.source_addr, 0) header struct.pack(BBHI, 0x02, 0xFD, 0x0005, len(payload)) self.tcp_sock.send(header payload) resp self.tcp_sock.recv(1024) _, _, ptype, _ struct.unpack(BBHI, resp[:8]) if ptype 0x0006: _, code struct.unpack(HB, resp[8:11]) return code 0x10 return False def send_uds(self, target_addr, uds_data): payload struct.pack(HH, self.source_addr, target_addr) uds_data header struct.pack(BBHI, 0x02, 0xFD, 0x8001, len(payload)) self.tcp_sock.send(header payload) resp self.tcp_sock.recv(4096) return resp[12:] # 跳过头部8字节和地址4字节 def close(self): if self.tcp_sock: self.tcp_sock.close()这个模板把连接、激活、发送、关闭封装成方法实际使用时只需要实例化并调用。参数上source_addr默认 0x0E00target_addr根据实际 ECU 填。send_uds返回的是纯 UDS 响应数据方便后续解析。5.3 一个容易被忽略的验证习惯最后说一个我踩过坑之后养成的习惯每次改完参数先只发车辆识别和路由激活确认这两步通了再发诊断报文。很多人一上来就发完整诊断流程失败了不知道卡在哪一步。分步验证虽然多花几分钟但排查问题时能直接定位到具体环节省下的时间远不止这几分钟。另外DoIP 的协议版本字节和取反字节一定要成对检查我见过不止一次因为版本号写错导致所有报文被静默丢弃的情况。抓包时先看这两个字节能排除掉相当一部分「没反应」的问题。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑