EOL总结:SOME/IP 笔记
一、SOMEIP 知识
1、SOME/IP的报文格式
SOME/IP的报文由 消息 ID Message ID(Service ID / Method ID)[32 bit] + 消息长度 Length[32 bit] + 请求 ID Request ID(Client ID / Session ID)[32 bit] + 协议版本 Protocol Version[8 bit] + 接口版本 Interface Version[8 bit] + 消息类型 Message Type[8 bit] + 返回码 Return Code[8 bit] + 消息体 Payload[variable size] 组成。除了消息体,其他组成消息头部。
-
消息长度:(从Request ID开始到Payload结束)
-
SOME/IP 服务分为服务提供者和服务调用者,用以区分相同消息的不同调用。Rquest ID = Client ID + Session ID.
-
Message Type,用于标识消息的类型,分别为 5 种类型:
-
1)请求类型(0x00):需要响应的请求;
-
2)无需响应的请求(0x01);
-
3)通知(0x02):无需响应的通知/事件回调;
-
4)响应(0x80):响应体
-
5)错误
-
SOME/IP 的消息类型可分为 Method 类型、Event 类型和 Field 类型。当为 Method 类型时(第 1 bit = 0),Message ID = Service ID + Method ID;当为 Event类型时(第 1 bit = 1),Message ID = Service ID + Event ID。三种消息类型和通信机制之间的映射关系如下:
-
传输协议:SOME/IP协议属于应用层协议,可基于 TCP/UDP。
2、SOME/IP 控制报文:SOME/IP-SD
-
SOME/IP-SD 解决的是 1)Client 如何发现服务;2)当服务不可用时,如何通知 Client;3)Client 如何订阅事件。即 SOME/IP-SD 用来实现服务发现和事件订阅机制。
-
SOME/IP-SD 报文格式如下:
2.1、Service Entry 用于服务发现
- 报文有个重要的 Type 字段(8bits):1)若 Service 就绪,会主动发出 OfferService 通知组播内其他节点该服务已启动,可以创建连接;2)若网络未收到相关服务的 OfferService,而 Client 又需要访问该服务,则 Client 发出 FindService 主动寻找服务,此时若 Service 就绪,则会回复 OfferService 报文;3)若服务不可用,会主动发出 StopOfferService 报文通知组播内其他节点该服务不可用,停止发送请求,并取消订阅。
2.2、Eventgroup Entry 用于事件订阅:
- 报文有个重要的 Type 字段(8bits):1)当 Client 收到服务 OfferService 之后,Client 可以发送 Subscribe 报文主动跟 Service 订阅感兴趣的事件组,也可发送 StopSubscribe 报文来通知 Service。2)Service 收到 Client 的 Subscribe 报文之后,需要先判断是否符合可订阅的条件,若满足,则返回 SubscribeAck,否则返回 SubscribeEventgroupNack 告知。3)当事件组的事件准备就绪之后,Service 会以某种约定好的形式发送相关事件给 Client。
3、FireAndForget
参考链接:https://geek-docs.com/redis/redis-ask-answer/3_redis_difference_between_fireandforget_and_async_behavior_for_publishing.html # Redis 发布过程中的FireAndForget和Async行为的区别
MCU1Misc.fidl 文件使用 fireAndForget 设计模式定义 SOC 发送数据给 MCU 的方法 EOLRingDataSoCToMCU:
method EOLRingDataSoCToMCU fireAndForget {
in {
UInt32 FCTDataLen
array_uint8 array_so
}
}
- FireAndForget:即发即忘
FireAndForget是指在消息发布之后,发布者不会再等待消息是否被成功投递给订阅者的确认。它是一种快速而简单的发布方式,适用于对消息可靠性要求不高,只需保证消息尽可能快地发给订阅者的场景。
二、CommonAPI 知识
参考链接1:https://www.jianshu.com/p/44b80df683c2 # CommonAPI入门
参考链接2:https://juejin.cn/post/7310156414251876352 # 深入理解CommonAPI 设计:打造高效、灵活的进程间通信
参考链接3:https://blog.csdn.net/djfjkj52/article/details/120698447 # SOME/IP不等同于SOA,CommonAPI-RPC通信和vsomeip基于消息通信
参考链接4:https://blog.csdn.net/xllhd100s/article/details/112798677?spm=1001.2014.3001.5502 # CommonAPI-SomeIP 使用
- CommonAPI C++是一个标准化的C++ API规范,用于开发分布式的应用程序。这些应用程序通过进程间通信的中间件进行通信。
- CommonAPI C++ 依赖
Franca-IDL
来描述静态接口以及通信和协议配置参数。通过代码生成器(CommonAPI-Tools)生成 proxy 和 stub- 代码。 - CommonAPI C++ 包括 CommonAPI C++ Core(与中间件无关)和 CommonAPI C++ Binding(与中间件相关)。Binding 代码生成器需要待定的中间件参数,参数定义在Francal 部署文件中(*.fdepl)
- GENIVI的 CommonAPI C++是基于vsomeip实现的RPC框架
三、SOMEIP 实战
EOL 的 eol_someip_client.cpp
的文件树如下:
├── build.sh
├── CMakeLists.txt
├── config
│ ├── commonapi.ini
│ └── vsomeip-client.json
├── fidl
│ ├── MCU1Misc.fdepl
│ └── MCU1Misc.fidl
├── run_client.sh
├── src
│ └── eol_someip_client.cpp
└── src-gen
└── v1
└── commonapi
├── MCU1Misc.hpp
├── MCU1MiscProxyBase.hpp
├── MCU1MiscProxy.hpp
├── MCU1MiscSomeIPDeployment.cpp
├── MCU1MiscSomeIPDeployment.hpp
├── MCU1MiscSomeIPProxy.cpp
├── MCU1MiscSomeIPProxy.hpp
├── MCU1MiscSomeIPStubAdapter.cpp
├── MCU1MiscSomeIPStubAdapter.hpp
├── MCU1MiscStubDefault.hpp
└── MCU1MiscStub.hpp
1、配置文件 config
commonapi.ini
与 vsomeip-client.json
属于 vsomeip 配置,MCU1Misc.fidl
属于服务的配置。vsomeip的配置关注网络通信和服务发现的底层细节,而IDL文件的配置关注服务接口的定义和数据的描述。
- 交互点:在使用vsomeip实现基于CommonAPI的服务时,IDL文件定义的服务接口将被转换为vsomeip可以理解的格式,并通过vsomeip的通信机制进行传输。此时,vsomeip的配置将确保这些服务接口能够正确地被发现和调用。
参考链接:https://blog.csdn.net/zjfengdou30/article/details/125912203 # vsomeip源码梳理 -- application初始化
1.1 commonapi.ini
这是用来配置 CommonAPI 相关应用程序的文件。CommonAPI 用于定义跨多个通信协议和编程语言的接口。
[default]
binding=someip // 绑定 CommonAPI 使用的协议为 SOMEIP
[logging]
console = true
file = log/CommonAPISomeIP.log
dlt = false // 不启动 DLT(Diagnostic Log and Trace)
level = verbose
1.2 vsomeip-client.json
vsomeip-client.json
用于配置 VSomeIP 客户端的行为和参数。VSomeIP 基于 UDP,允许服务在不同组件之间发布和发现。
{
"unicast": "192.168.1.11", // 指定客户端单播地址
"netmask": "255.255.255.0",
"logging": {
"level": "debug",
"console": "true",
"file": {
"enable": "true",
"path": "log/vsomeipClient.log"
},
"dlt": "true"
},
"applications": [ // 指定客户端要使用的应用程序
{
"name": "proxy.SOC",
"id": "0x1343" // SomeIP 服务 ID
}
],
"routing": "proxy.SOC", // 指定路由到的应用程序名称
"service-discovery": { // 配置服务发现的行为
"enable": "true", // 启用服务发现
"multicast": "224.244.224.245", // 指定用于服务发现的组播地址
"port": "30490", // 用于服务发现的 UDP 端口
"protocol": "udp"
}
}
1.3 MCU1Misc.fidl
参考链接:https://blog.csdn.net/sinat_20184565/article/details/93907495 # FIDL指南
FIDL 文件用来定义接口以及相关数据结构。MCU1Misc.fidl
定义了一个简单的 CommonAPI 方法,该接口支持一个广播和一个方法,并定义了一个数组类型作为参数的一部分。
使用这个 FIDL 文件,你可以使用 CommonAPI 工具(如 capigen)来生成代码,以便在应用程序中实现这个接口。
package commonapi
interface MCU1Misc { // 定义名为 MCU1Misc 的接口
version { major 1 minor 0 }
broadcast EOLRingDataMCUToSoC{ // 定义一个需要接收报文的广播(MCU 发给 SOC 的广播)
out { // 定义此广播的输出参数
UInt32 FCTDataLen
array_uint8 array_mcu
}
}
method EOLRingDataSoCToMCU fireAndForget{ // 定义一个即发即忘方法,fireAndForget 语义表示该方法发送请求后无需等待响应
in { // 定义方法的输入参数
UInt32 FCTDataLen
array_uint8 array_soc
}
}
array array_uint8 of UInt8
}
1.4 MCU1Misc.fdepl
import "platform:/plugin/org.genivi.commonapi.someip/deployment/CommonAPI-SOMEIP_deployment_spec.fdepl"
import "MCU1Misc.fidl"
define org.genivi.commonapi.someip.deployment for interface commonapi.MCU1Misc { // 定义一个针对 MCU1Misc 接口的部署描述符
SomeIpServiceID = 49414 // MCU1Misc 的 SOMEIP 服务 ID
method EOLRingDataSoCToMCU { // 定义一个 SOMEIP 方法
SomeIpMethodID = 1 // 定义方法 ID
}
broadcast EOLRingDataMCUToSoC{ // 定义一个广播事件
SomeIpEventID = 32769 // 定义事件 ID
SomeIpEventGroups = { 1 } // 定义事件组
}
array array_uint8{ // 定义一维数组
SomeIpArrayMaxLength = 1024
SomeIpArrayLengthWidth = 0
}
}
define org.genivi.commonapi.someip.deployment for provider as Service { // 定义了一个服务提供者
instance commonapi.MCU1Misc { // 实例化接口
InstanceId = "MCU1Misc"
SomeIpInstanceID = 385 // 实例 ID
}
}
1、someip_client 实例
1.1 vsomeip-client.json
{
"unicast": "192.168.1.11, # 本地网络接口 IP
"netmask": "255.255.255.0, # 子网掩码
"logging": {
"level": "debug",
"console": "true",
"file": {
"enable": "true",
"path": "log/vsomeipClient.log"
},
"dlt": "true" # 启用数据链路跟踪(Diagnostic Log and Trace, Dlt 模块)
},
"applications": [
{
"name": "proxy.SOC",
"id": "0x1343"
}
],
"routing": "proxy.SOC", # 将数据路由到名为 "proxy.SOC" 的应用程序
"service-discovery":{
"enable": "true", # 启用服务发现功能
"multicast": "224.244.224.245", # 多播地址
"port": "30490",
"protocol": "udp" # 指定服务发现使用 UDP 协议
}
}
6.2 someip_client 入口程序
6.2.1 CommonAPI 的 proxy 和 stub 概念
Proxy(代理):是客户端的本地对象,代表远程服务器,客户端通过调用 Proxy 的方法来与远程服务器通信。
- Proxy 负责将客户端的请求数据序列化,并通过中间件发送到远程服务端;
- 客户端无需知道底层通信方式,只需调用本地对象一样调用 Proxy 即可
Stub(存根):是服务端的本地对象,用于接收来自客户端的请求。
- Stub 负责反序列化客户端的请求数据;
- 服务端无需关心底层通信方式,只需处理实际业务逻辑即可
someip 客户端的实现仅需 3 步:
1)实例化客户端代理
pClient
(SomeIPClientProxy
)是客户端代理基类,并不是具体的 proxy,它负责处理客户端与服务端的通信;
std::shared_ptr<CommonAPI::ServiceInterface::SomeIPClientProxy> pClient = std::make_shared<CommonAPI::ServiceInterface::SomeIPClientProxy>();
2)发现服务
Client 发出 FindService 主动寻找服务,此时若 Service 就绪,则会回复 OfferService 报文。
- 通过代理基类主动寻找服务并将其作为代理实例。
g_MCU1Client
是代理实例,用来调用服务端的方法以及接收服务端的事件;
g_MCU1Client = pClient->FindService<v1::commonapi::MCU1MiscProxy>(2000);
- 发现服务后,还需判断服务是否可用。通过
isAvailable()
判断服务是否被代理可用。
while (!g_MCU1Client->isAvailable())
3)订阅服务
当 Client 收到服务 OfferService 之后,Client 可以发送 Subscribe 报文主动跟 Service 订阅感兴趣的事件组。Service 收到 Client 的 Subscribe 报文之后,需要先判断是否符合可订阅的条件,若满足,则返回确认报文。
fdepl 文件定义感兴趣的事件 EOLRingDataMCUToSoCEvent
的事件 ID 为 32769
,事件组为 1
。
- 若服务可用,则订阅服务。订阅服务后,一旦事件发生,将会调用处理事件
g_MCU1Client->getEOLRingDataMCUToSoCEvent().subscribe(
[&](const uint32_t &len, const v1::commonapi::MCU1Misc::array_uint8 &data) { process_data(len, data); });
int main()
{
...
std::shared_ptr<v1::commonapi::MCU1MiscProxy<>> g_MCU1Client= nullptr;
// step1:pClient(SomeIPClientProxy)并不是具体的 proxy,它负责处理客户端与服务端的通信
std::shared_ptr<CommonAPI::ServiceInterface::SomeIPClientProxy> pClient = std::make_shared<CommonAPI::ServiceInterface::SomeIPClientProxy>();
// step2:g_MCU1Client 是代理实例,用来调用服务端的方法以及接收服务端的事件;
// 一个 SOMEIP 服务是一个服务端。发现服务, 设置超时时长为 2s
g_MCU1Client = pClient->FindService<v1::commonapi::MCU1MiscProxy>(2000);
if (!g_MCU1Client) return -1;
while (!g_MCU1Client->isAvailable())
{
std::this_thread::sleep_for(std::chrono::microseconds(1000 *100));
}
// step3:订阅服务,一旦服务发生,将会调用处理函数
g_MCU1Client->getEOLRingDataMCUToSoCEvent().subscribe(
[&](const uint32_t &len, const v1::commonapi::MCU1Misc::array_uint8 &data) { process_data(len, data);
});
return 0;
}
6.3 对 SOMEIP 进行数据校验
SOC 需通过校验 MCU 发来的 SOMEIP 数据报文来确保数据的正确性。
7、CommonAPI::ServiceInterface::SomeIPClientProxy
在 CommonAPI 框架中,服务接口通常通过 IDL (Interface Definition Language) 文件定义,然后这些定义被转换为 C++ 或 C 的源代码。生成的代码包括代理(Proxy)和存根(Stub)类,用于在客户端和服务端之间传递消息。
CommonAPI::ServiceInterface::SomeIPClientProxy
是 CommonAPI 框架中的客户端代理类,它用于通过 SOME/IP 协议与远程服务进行通信。它负责以下任务:
- 服务发现:通过 SOME/IP 机制在网络中查找并连接到特定的远程服务。
- 消息传递:将客户端的请求序列化为 SOME/IP 消息,并通过网络发送到远程服务。同时,它也将从远程服务接收到的响应反序列化为客户端可以理解的格式。
- 事件订阅:允许客户端订阅远程服务的事件。当这些事件发生时,远程服务会发送消息到客户端,客户端的代理类会负责处理这些消息。
- 错误处理:处理与服务通信期间可能发生的错误,例如连接丢失或请求超时。
如 6.2 的例子,pClient
被用来查找 v1::commonapi::MCU1MiscProxy
类型的远程服务,并设置了一个对远程服务事件的订阅。一旦找到并连接到服务,客户端就可以通过 MCU1MiscProxy
代理类的接口与远程服务进行通信,调用远程服务的方法或订阅其事件。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 【自荐】一款简洁、开源的在线白板工具 Drawnix
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本