3、SD报文结构解析:Entry、Option、Subscriber/Offer/StopOffer等关键字段
SOME/IP服务发现(SD)报文,说白了就是车载ECU之间互相喊话的“小纸条”。你想想看,一个域控制器要找到另一个域控制器提供的服务,总得有个约定好的格式吧?这个格式就是SD报文。今天我就带你把这张“小纸条”拆开看看,里面到底写了什么。
3.1 SD报文整体骨架
先看一个SD报文的完整结构。我个人习惯把它分成三块:头部、Entry数组、Option数组。
| 字段 | 长度(字节) | 说明 |
|---|---|---|
| Message ID | 4 | 固定为0xFFFF8100(SD专用) |
| Length | 4 | 从Entry数组开始到报文末尾的总长度 |
| Request ID | 4 | Client ID + Session ID,用于匹配请求/响应 |
| Protocol Version | 1 | 当前固定为0x01 |
| Interface Version | 1 | SD协议自身版本,通常也是0x01 |
| Message Type | 1 | 0x02 = Notification(SD报文专用) |
| Return Code | 1 | 0x00(SD报文不使用错误码) |
| Reserved | 4 | 全0填充 |
| Entry Array | 可变 | 核心内容,每条Entry 16字节 |
| Option Array | 可变 | 附加信息,每条Option长度可变 |
嗯,这里要注意:Entry和Option是分开存放的。Entry先写,Option后写,中间没有分隔符。解析的时候全靠Length字段来定位。
3.2 Entry:服务发现的“主语”
Entry是SD报文的灵魂。它告诉别人“我在找什么服务”或者“我提供了什么服务”。每条Entry固定16字节,结构如下:
| 偏移(字节) | 字段 | 长度(位) | 说明 |
|---|---|---|---|
| 0 | Type | 8 | Entry类型:0x00=Find, 0x01=Offer, 0x06=StopOffer, 0x04=Subscribe, 0x05=SubscribeAck, 0x07=SubscribeNack |
| 1 | Index 1st Option | 8 | 第一个Option在Option数组中的索引 |
| 2 | Index 2nd Option | 8 | 第二个Option的索引(没有则填0xFF) |
| 3 | Number of Option 1 | 4 | 第一个Option组包含的Option数量 |
| 3 | Number of Option 2 | 4 | 第二个Option组包含的Option数量 |
| 4-7 | Service ID | 32 | 服务ID(16位)+ Instance ID(16位) |
| 8-11 | Major Version | 8 | 主版本号 |
| 8-11 | TTL | 24 | 生存时间(秒),0表示立即失效 |
| 12-15 | Minor Version | 32 | 次版本号(仅用于Find/Offer/Subscribe) |
关键点:Entry的Type字段决定了整个报文的语义。同一个报文中可以混合多种Entry类型,但解析时必须按顺序处理。
3.3 三种核心Entry类型
3.3.1 Offer Service Entry
当ECU启动并准备好提供服务时,它会发送Offer报文。Type=0x01。我记得第一次调试时,怎么都收不到对端的Offer,后来发现是TTL设成了0——嗯,这等于告诉别人“我马上就不干了”。
- Service ID + Instance ID:唯一标识一个服务实例
- TTL:通常设为3秒(后续靠周期性重发维持)
- Major/Minor Version:客户端必须匹配Major版本才能订阅
3.3.2 Subscribe Eventgroup Entry
客户端想接收某个服务的事件通知时,发送Subscribe报文。Type=0x04。这里有个坑:Subscribe Entry不包含IP和端口信息,这些信息放在Option里。
我曾经踩过的坑:Subscribe报文中的Reserved字段必须填0。有一次我为了“优化”填了个非零值,结果对端直接丢弃了报文。调试了整整一个下午才发现。
3.3.3 Stop Offer Entry
服务端要下线时,发送StopOffer。Type=0x06。它的TTL固定为0,表示立即失效。我个人习惯在StopOffer中只带一个IPv4 Endpoint Option,告诉客户端“别再来找我了”。
3.4 Option:服务发现的“状语”
Option用来描述Entry的细节——比如IP地址、端口、通信协议等。没有Option,Entry就是个空壳子。
| Option Type | Type值 | 长度(字节) | 用途 |
|---|---|---|---|
| IPv4 Endpoint | 0x04 | 12 | 提供IPv4地址 + UDP/TCP端口 |
| IPv6 Endpoint | 0x06 | 24 | 提供IPv6地址 + UDP/TCP端口 |
| Multicast | 0x14 | 12 | 组播地址 + 端口(用于事件通知) |
| Configuration | 0x01 | 可变 | 键值对形式的配置参数 |
| Load Balancing | 0x09 | 8 | 优先级 + 权重(用于冗余服务) |
你想想看,一个Offer Entry配上IPv4 Endpoint Option,就完整表达了:“我在192.168.1.100:30509提供Service ID=0x1234的服务”。
3.5 Entry与Option的关联机制
这是初学者最容易搞混的地方。Entry中的Index 1st Option和Index 2nd Option指向的是Option数组中的起始位置,而Number of Option 1/2告诉你有几个连续的Option属于这个Entry。
我的调试技巧:抓包时先数Option数组的总长度,再根据每个Entry的Index字段反推。如果发现Index超出了Option数组范围,那一定是发送端组包逻辑出了问题。
3.6 完整报文示例
下面是一个Offer报文的实际例子(十六进制):
// 头部
FF FF 81 00 // Message ID = 0xFFFF8100
00 00 00 28 // Length = 40字节
00 00 00 01 // Request ID
01 // Protocol Version = 1
01 // Interface Version = 1
02 // Message Type = Notification
00 // Return Code = 0
00 00 00 00 // Reserved
// Entry Array(1条Offer Entry)
01 // Type = Offer
00 // Index 1st Option = 0
FF // Index 2nd Option = 0xFF(无)
10 // Num Opt 1 = 1, Num Opt 2 = 0
00 01 // Service ID = 0x0001
00 01 // Instance ID = 0x0001
01 // Major Version = 1
00 00 0E 10 // TTL = 3600秒(1小时)
00 00 00 01 // Minor Version = 1
// Option Array(1条IPv4 Endpoint Option)
00 04 // Type = IPv4 Endpoint
00 08 // Length = 8字节(不含头部)
00 00 // Reserved
C0 A8 01 64 // IPv4 = 192.168.1.100
77 1D // Port = 30493
3.7 知识结构图
下面这张图帮你理清SD报文的整体脉络:
3.8 避坑指南
最后,分享几个我实际项目中遇到的坑:
- Option索引越界:Entry中的Index字段指向Option数组,但Option数组可能为空。如果Index不为0xFF而Option数组长度为0,报文就是非法的。
- TTL与重发周期不匹配:TTL设了3秒,但重发周期设了5秒——这会导致服务在客户端眼中反复“消失又出现”。
- Subscribe缺少Endpoint Option:客户端只发了Subscribe Entry,没带Endpoint Option。服务端不知道往哪里发事件通知,订阅等于白费。
- StopOffer带了非零TTL:StopOffer的TTL必须为0。我见过有人复制Offer报文改Type忘了改TTL,结果对端以为服务还在。
一句话总结:SD报文 = Entry(做什么)+ Option(怎么做)。解析时先读Entry确定语义,再根据Index找到对应的Option获取细节。顺序不能乱,索引不能错。