2. 物理层与传输层:以太网物理层基础、TCP/UDP在DoIP中的应用、Socket通信原理
好,咱们开始聊第二章节。说实话,很多做诊断的工程师一听到「以太网物理层」就开始头疼,觉得这是搞通信的人该管的事。我以前也这么想,直到有一次在台架上死活连不上DoIP,折腾了两天,最后发现是网线接头松了……嗯,从那以后我老实了。物理层的东西,你多少得懂一点。
2.1 以太网物理层基础
DoIP跑在以太网上,那以太网物理层到底是个啥?说白了,它就是负责把0和1变成电信号或者光信号,然后扔到线上去。咱们做诊断,最常接触的是100BASE-TX(百兆)和1000BASE-T(千兆)。
我个人习惯,在搭建测试环境时优先选千兆。为什么?因为DoIP诊断数据包虽然不大,但如果你同时刷写多个ECU,百兆的带宽很快就撑不住了。你想想看,一个UDS诊断请求才几个字节,但刷写一个固件动辄几十兆,百兆链路就成了瓶颈。
这里有个关键参数你得记住:MTU(最大传输单元)。标准以太网MTU是1500字节。DoIP的报文如果超过这个值,就得拆包。我在项目中遇到过,某个ECU的DoIP实现有bug,超过1400字节的报文就丢包。排查了整整一个下午……
物理层避坑指南:
- 网线质量:别用那种几块钱的扁线,屏蔽层都没有,干扰大得很
- 接口类型:DoIP通常用RJ45,但有些车载以太网用BroadR-Reach(单对线),别搞混了
- 自动协商:大部分交换机支持,但如果你直连PC和ECU,记得确认两端速率一致
我的小技巧:测试环境里备一根已知好的短网线(0.5米左右),出问题了先换线试试。我曾经因为一根坏网线浪费了三个小时,后来学乖了。
2.2 TCP/UDP在DoIP中的应用
DoIP协议里,TCP和UDP各有各的活。咱们分开说。
2.2.1 UDP——发现与宣告
UDP在DoIP里主要干两件事:车辆发现和车辆宣告。你想想看,诊断仪连到车上,怎么知道车上都有哪些ECU支持DoIP?靠的就是UDP广播。
诊断仪发一个UDP广播报文到端口13400,车上所有支持DoIP的ECU都会回复。这个过程不需要建立连接,发出去就行。UDP的特点就是快,但不可靠。不过对于发现服务来说,丢一两个包无所谓,多试几次就行。
我记得有一次,客户说他们的诊断仪找不到ECU。我抓包一看,UDP广播被交换机拦截了。原来那台交换机的IGMP Snooping配置有问题,把广播包当组播给过滤了。嗯,这种坑防不胜防。
2.2.2 TCP——诊断通信的主力
一旦发现阶段结束,真正的诊断通信(UDS请求/响应)就走TCP了。为什么用TCP?因为诊断数据不能丢。你发一个「读取故障码」的请求,结果丢了,ECU没收到,那诊断仪就傻等了。
TCP提供的是可靠的、面向连接的传输。DoIP使用TCP端口13400。建立连接后,诊断仪和ECU之间就有一条稳定的通道。
这里有个细节:TCP的Nagle算法。默认情况下,TCP会把小包攒起来再发,以减少网络开销。但诊断请求通常很小,攒包会导致延迟。我建议在DoIP的Socket上禁用Nagle算法(设置TCP_NODELAY)。
注意:DoIP协议规定,一个TCP连接只能处理一个诊断逻辑连接。如果你想并发诊断多个ECU,就得建立多个TCP连接。别想着在一个连接里来回切换,协议不支持。
| 特性 | UDP | TCP |
|---|---|---|
| 连接方式 | 无连接 | 面向连接 |
| 可靠性 | 不可靠 | 可靠 |
| DoIP用途 | 车辆发现/宣告 | 诊断通信 |
| 端口 | 13400 (UDP) | 13400 (TCP) |
| 典型报文大小 | 几十字节 | 几百到几千字节 |
2.3 Socket通信原理
Socket,中文叫套接字,但大家更习惯叫Socket。它是操作系统提供的一个编程接口,让你能通过代码收发网络数据。
在DoIP自动化测试中,你肯定要写代码来模拟诊断仪或者ECU。这时候Socket就是你的武器。
2.3.1 创建一个TCP Socket
以Python为例,创建一个TCP Socket就几行代码:
import socket
# 创建TCP Socket
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
# 连接到ECU(假设IP是192.168.0.10,端口13400)
sock.connect(('192.168.0.10', 13400))
# 发送DoIP报文
sock.send(doip_packet)
# 接收响应
response = sock.recv(4096)
# 关闭连接
sock.close()
看着简单吧?但实际用起来坑不少。我遇到过最典型的问题:recv()不一定会一次收完所有数据。TCP是流式协议,你发一个包,对端可能分两次收到。所以你得自己处理粘包和拆包。
处理粘包的通用做法:
- 先读固定长度的头部(DoIP头部是8字节)
- 从头部解析出Payload长度
- 再循环读取Payload,直到收完指定长度
2.3.2 UDP Socket的创建
UDP Socket更简单,不需要connect:
import socket
# 创建UDP Socket
udp_sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
# 发送车辆发现请求(广播)
udp_sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
udp_sock.sendto(discovery_packet, ('255.255.255.255', 13400))
# 接收响应
data, addr = udp_sock.recvfrom(4096)
print(f"收到来自 {addr} 的响应")
UDP的recvfrom会返回数据和对端地址。这个地址就是你后续建立TCP连接的目标。
经验之谈:写自动化测试脚本时,记得给Socket设置超时。不然ECU没响应,你的脚本就卡死了。我一般设3秒超时,够用。
2.3.3 Socket的阻塞与非阻塞
默认情况下,Socket是阻塞模式。也就是说,你调用recv()时,如果没有数据,程序就停在那里等。这在单线程脚本里没问题,但如果你要同时跟多个ECU通信,就得用非阻塞或者多线程。
我个人习惯用settimeout(),既不是纯阻塞也不是纯非阻塞,而是带超时的阻塞:
sock.settimeout(2.0) # 2秒超时
try:
data = sock.recv(4096)
except socket.timeout:
print("ECU没响应,超时了")
这样写,代码清晰,也容易控制流程。
2.4 小结
这一章咱们聊了物理层的网线和MTU,讲了UDP和TCP在DoIP里的分工,最后手把手写了Socket代码。说白了,物理层是地基,传输层是管道,Socket是工具。地基不稳,管道再粗也没用;工具不趁手,干活就费劲。
下一章,咱们要进入DoIP协议的核心——报文格式和状态机。那才是真正烧脑的地方。做好准备。