3. 网络层与传输层:地址分配、端口号与Socket通信

好,咱们进入第三章节。说实话,很多做诊断的工程师,一开始都只关注应用层的那些诊断服务,比如读故障码、刷写数据。但我要说,网络层和传输层才是DoIP的骨架。你应用层写得再漂亮,如果IP地址配错了,或者端口号搞混了,车上的ECU根本连不上你,那一切都是白搭。

我个人习惯,在开始写任何DoIP代码之前,先把网络层的这张图刻在脑子里。咱们先看个总览,再拆开细讲。

DoIP 网络层与传输层协议栈 应用层 (UDS诊断服务) 传输层 (TCP/UDP) TCP 13400 (可靠连接) | UDP 13400 (发现/激活) 网络层 (IPv4 / IPv6) 地址分配:链路本地 | DHCP | 自动配置 数据链路层 (Ethernet MAC) 核心:诊断仪与ECU通过Socket通信,绑定IP:Port,完成数据收发

3.1 IPv4与IPv6地址分配

DoIP支持两种IP版本:IPv4和IPv6。你可能会问,现在IPv6还没普及,是不是只用IPv4就够了?嗯,这里要注意,很多新车型,尤其是欧洲车,已经开始强制要求IPv6了。我建议你两个都得懂。

3.1.1 IPv4地址分配

在DoIP的典型场景里,诊断仪和ECU通常是在一个封闭的车辆网络里通信。没有路由器,没有DHCP服务器。那IP地址怎么来?

  • 链路本地地址 (Link-Local):这是最常用的方式。ECU上电后,会自动在 169.254.x.x/16 这个段里选一个地址。诊断仪也一样。说白了,就是设备自己协商,不需要服务器。
  • DHCP:如果车辆网络里有DHCP服务器(比如T-Box),ECU也可以从服务器获取地址。但我在项目里很少见到这种用法,因为增加复杂度了。
  • 静态配置:有些老项目,直接给ECU写死一个IP。我不推荐这么做,万一地址冲突,排查起来很头疼。
我的经验: 我曾经在一个项目里,ECU的IPv4链路本地地址老是跟诊断仪冲突。后来发现是ECU的随机种子算法有问题,导致两台设备选了同一个地址。解决办法是让ECU在地址冲突后,强制等待几秒再重新选择。这个坑,你设计时最好也考虑进去。

3.1.2 IPv6地址分配

IPv6在DoIP里越来越重要。它的地址分配方式跟IPv4不太一样。

特性 IPv4 (链路本地) IPv6 (链路本地)
地址范围 169.254.0.0/16 FE80::/10
地址生成 随机选择 + ARP探测 基于MAC地址 (EUI-64) 或随机
冲突检测 ARP (地址解析协议) DAD (重复地址检测)
配置方式 自动配置 (Auto-IP) 无状态地址自动配置 (SLAAC)

你看,IPv6的链路本地地址是自动生成的,通常基于MAC地址。这意味着每个ECU的IPv6地址几乎是唯一的,冲突概率极低。我个人更喜欢IPv6这一点,省心。

3.2 TCP/UDP端口号定义

端口号是传输层的核心。DoIP标准里,端口号定义得非常清楚。你千万别自己乱改。

  • UDP 13400:用于车辆发现和节点激活。说白了,就是诊断仪刚上车,不知道有哪些ECU,就通过UDP广播一个请求,ECU们收到后回复自己的信息。
  • TCP 13400:用于所有后续的诊断通信。一旦连接建立,所有的UDS请求和响应,都走这个TCP连接。

为什么用两个协议?

UDP是无连接的,适合广播和快速握手。TCP是可靠的,适合传输诊断数据。你想想看,如果诊断刷写时丢了一个包,那整个ECU可能就变砖了。所以,关键数据必须走TCP。

3.3 Socket通信基础

好了,理论说完了,咱们来点实际的。Socket就是操作系统提供的一个编程接口,让你能收发网络数据。在DoIP里,你本质上就是在写Socket程序。

3.3.1 创建Socket

无论是诊断仪还是ECU,第一步都是创建一个Socket。代码很简单,但类型不能搞错。

// 创建UDP Socket (用于车辆发现)
int udp_sock = socket(AF_INET, SOCK_DGRAM, 0);

// 创建TCP Socket (用于诊断通信)
int tcp_sock = socket(AF_INET, SOCK_STREAM, 0);

注意看,SOCK_DGRAM 是UDP,SOCK_STREAM 是TCP。我见过有人把这两个搞混,结果UDP当TCP用,数据收不到还找不到原因。

3.3.2 绑定地址与端口

创建完Socket,需要绑定一个本地地址和端口。ECU通常绑定在 0.0.0.0:13400,意思是监听所有网络接口上的13400端口。

struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(13400);        // 端口号
addr.sin_addr.s_addr = INADDR_ANY;   // 绑定所有接口

bind(tcp_sock, (struct sockaddr*)&addr, sizeof(addr));
避坑指南: 我曾经在一个项目里,ECU绑定了端口13400,但诊断仪死活连不上。查了半天,发现是另一个进程占用了13400端口。记住,端口是独占资源,一个端口只能被一个进程绑定。如果你的ECU有多个诊断通道,需要用不同的端口,或者用同一个Socket处理多连接。

3.3.3 监听与连接 (TCP)

ECU作为服务器,需要监听连接请求。诊断仪作为客户端,主动发起连接。

// ECU端:监听
listen(tcp_sock, 5);  // 最大挂起连接数

// 诊断仪端:连接
connect(tcp_sock, (struct sockaddr*)&server_addr, sizeof(server_addr));

这里有个细节:DoIP标准规定,一个ECU通常只允许一个诊断仪连接。所以,当第二个诊断仪尝试连接时,ECU应该拒绝。这个逻辑你需要在代码里实现。

3.3.4 数据收发

连接建立后,就是收发数据了。TCP用 send() 和 recv(),UDP用 sendto() 和 recvfrom()。

// TCP发送诊断请求
uint8_t uds_request[] = {0x22, 0xF1, 0x90};  // 读取VIN码
send(tcp_sock, uds_request, sizeof(uds_request), 0);

// TCP接收诊断响应
uint8_t buffer[4096];
int len = recv(tcp_sock, buffer, sizeof(buffer), 0);
小技巧: TCP是流协议,没有消息边界。你发送一个100字节的包,对端可能分两次收到。所以,接收方一定要做粘包处理。我习惯的做法是:先接收一个固定长度的头部,从头部里解析出负载长度,再接收负载。DoIP协议本身就有这个头部结构,咱们后面章节会细讲。

3.4 本章小结

这一章的内容,说白了就是三件事:地址怎么配、端口用哪个、Socket怎么写。IPv4和IPv6的地址分配方式不同,但核心都是让设备能互相找到。UDP 13400用于发现,TCP 13400用于诊断。Socket编程是基础,你只要记住创建、绑定、监听、连接、收发这五个步骤就行。

嗯,这里要再强调一下:别小看这些基础。我见过太多人,一上来就写应用层代码,结果网络层配置错了,折腾一整天。先把地基打牢,后面盖楼才稳。