赞
踩
SOME/IP 是 “Scalable service-Oriented MiddlewarE over IP” 的缩写,中文翻译为:基于 IP 的可扩展面向服务中间件。该中间件设计用于典型的汽车用例,并与 AUTOSAR 兼容(至少在有线格式级别上)。公众可访问的规范可从以下站点获取 http://some-ip.com/。在本 wiki 中,我们不想进一步探讨另一个中间件规范的原因,但想大致概述一下 SOME/IP 规范及其开源实现 vsomeip 的基本结构,而不要求完整性。
让我们从 SOME/IP 规范的三个主要部分开始:
原则上,SOME/IP 通信包括基于 IP 在设备或订阅者之间发送消息,参考以下图片:
在图中你可以看到两个设备(A 和 B);设备 A 向 B 发送一条 SOME/IP 消息并从设备 B 接收一条信息回来。底层传输协议可以是 TCP 或者 UDP;对于消息本身是没有区别的。现在我们假设设备 B 正在运行一个服务,该服务提供一个由这个消息从设备 A 调用的函数,返回的消息就是答案。
SOME/IP 消息有两部分:header 和 payload。
在图中你可以看到 header 由以下的标识符组成:
我们看到,正常函数调用有“请求”和“响应”,客户端已订阅的事件有通知消息。错误报告为正常响应,通知则带有适当的返回代码。
payload 包含序列化数据。该图显示了在传输的数据结构是仅具有基本数据类型的嵌套结构的简单情况下的序列化。在这种情况下就很简单:结构元素只是平展的,这意味着它们只是一个接一个地写入 payload 中。
在本节中主要有两点很重要,现将予以说明:
如上所述,底层传输协议可以是 UDP 或 TCP。在 UDP 情况下,SOME/IP 消息不分段;一个 UDP 数据包中可能包含多条消息,但一条消息的长度不能超过 UDP 数据包的长度(最多 1400 字节)。更大的消息必须通过 TCP 传输,在这种情况下,使用 TCP 的所有健壮性特征。如果在 TCP 流中发生同步错误,SOME/IP 规范允许调用所谓的神奇 Cookie 以便再次找到下一条消息的开关。
请注意,服务接口必须实例化,因为同一个接口可能有多个实例,所以必须为实例定义一个附加标识符(实例 ID)。但是实例 ID 不是 SOME/IP 消息头的一部分。实例通过传输协议的端口号进行标识;这意味着不可能在同一个端口上提供同一接口的多个实例。
现在请看下图,其中显示了基本的 SOME/IP 通信模式:
除了远程过程调用的标准“请求/响应”机制之外,还有事件的“发布/订阅”模式。注意到事件在 SOME/IP 协议中总是分组在事件组中;因此只能订阅事件组而不是订阅事件本身。SOME/IP 规范也定义了“字段”;在这种情况下,setter/getter 方法遵循“请求/响应”模式,更改的通知消息是事件。订阅本身通过 SOME/IP 服务发现完成。
SOME/IP Service Discovery 服务发现用于定位服务实例并检测服务实例是否正在运行,以及实现“发布/订阅”处理。这主要通过所谓的 offer 消息来完成;这意味着每个设备广播(多播)包含该设备提供的所有服务的消息。SOME/IP SD 消息通过 UDP 发送。如果客户端应用程序需要的服务目前尚未提供,则可以发送“查找消息”。其他的 SOME/IP SD 消息可用于发布或订阅事件组。
下图显示了一个 SOME/IP SD 消息的一般结构。
对于入门这应该足够了,更多的细节将在后面的示例中讨论或者可以参阅 SOME/IP SD 规范。
在开始实现介绍性示例之前,让我们先简单了解一下 SOME/IP 的 GENIVI 实现的基本结构——vsomeip。
如图所示,vsomeip 不仅涵盖设备之间的 SOME/IP 通信(外部通信),还包括内部进程间通信。两个设备通过所谓的通信端点(Endpoint)进行通信,通信端点确定所使用的传输协议(TCP 或 UDP)及其参数作为端口号或其他参数。所有这些参数都是可以在 vsomeip 配置文件(json 文件,请参阅 vsomeip 用户指南)中设置的配置参数。内部通信通过本地端点(Local Endpoint)完成,本地端点由使用 Boost.Asio 库的 unix domain socket 实现。由于这种内部通信不是通过中央组件(例如,像 D-Bus 守护进程)路由的,因此速度非常快。
只有当消息必须发送到外部设备时,中央 vsomeip 路由管理器才会获取消息,并分发来自外部的消息。每个设备只有一个路由管理器;如果未配置任何内容,则第一个运行的 vsomeip 应用程序也会启动路由管理器。
注意:vsomeip 并不实现数据结构的序列化!CommonAPI 的 SOME/IP 绑定涵盖了这一点,vsomeip 仅涵盖了 SOME/IP 协议和服务发现。
这是 SOME/IP 和 vsomeip 非常非常简短的概述。但对于第一次开始这已经足够了;进一步的细节将在示例中进行解释。
如前所述,vsomeip 需要 Boost.Asio 库,所以确保你已经在你的系统上安装了 BOOST (最低版本 1.55)。
译者:在 Ubuntu 系统中你可以使用以下指令安装 Boost 开发库:
sudo apt install -y libboost-dev libboost-system-dev \
libboost-thread-dev libboost-filesystem-dev \
libboost-log-dev libsystemd-dev
当 Boost 已经成功安装,你可以像往常一样毫无困难地构建 vsomeip:
cd vsomeip
mkdir build
cd build
cmake ..
make
这是可行的,但为了避免以后出现一些特殊问题,我建议在 CMake 调用中至少添加一个参数:
cmake -DENABLE_SIGNAL_HANDLING=1 ..
此参数确保您可以毫无问题地终止 vsomeip 应用程序(否则当你使用 Ctrl-C 停止应用程序时,可能无法正确删除共享内存段 /dev/shm/vsomeip
)。
译者:为了让后续的应用程序能够正常运行,建议将编译好的 vsomeip 库部署到你的 Ubuntu 系统中并更新库的缓存:
sudo make install
sudo ldconfig
创建第一个 vsomeip 应用程序;让我们称它为 service-example
:
service-example.cpp
#include <vsomeip/vsomeip.hpp>
std::shared_ptr< vsomeip::application > app;
int main() {
app = vsomeip::runtime::get()->create_application("World");
app->init();
app->start();
}
这非常简单:你必须首先创建一个应用程序对象,然后初始化并启动它。在创建 vsomeip 应用程序后,必须首先调用 init 方法,并执行以下步骤对其进行初始化:
为了启动消息处理,必须在 init 之后调用 start 方法。接收到的消息通过套接字进行处理,注册的回调用于将它们传递给用户应用程序。
准备好了
现在创建一个用于构建应用程序的 CMake 文件,它可能看起来像这样:
CMakeLists.txt (Example)
cmake_minimum_required (VERSION 2.8)
set (CMAKE_CXX_FLAGS "-g -std=c++11")
find_package (vsomeip3 3.1.20 REQUIRED)
find_package( Boost 1.55 COMPONENTS system thread log REQUIRED )
include_directories (
${Boost_INCLUDE_DIR}
${VSOMEIP_INCLUDE_DIRS}
)
add_executable(service-example ../src/service-example.cpp)
target_link_libraries(service-example vsomeip3 ${Boost_LIBRARIES})
照常进行(创建 build
目录,运行 CMake
构建),然后启动程序(service-example
),你应该在控制台上获得以下输出(或类似输出):
2022-08-11 10:04:42.254418 [info] Parsed vsomeip configuration in 0ms 2022-08-11 10:04:42.255106 [info] Configuration module loaded. 2022-08-11 10:04:42.255244 [info] Initializing vsomeip application "World". 2022-08-11 10:04:42.255868 [info] Instantiating routing manager [Host]. 2022-08-11 10:04:42.256312 [info] create_local_server Routing endpoint at /tmp/vsomeip-0 2022-08-11 10:04:42.256942 [info] Service Discovery enabled. Trying to load module. 2022-08-11 10:04:42.259474 [info] Service Discovery module loaded. 2022-08-11 10:04:42.259952 [info] Application(World, 0100) is initialized (11, 100). 2022-08-11 10:04:42.260123 [info] Starting vsomeip application "World" (0100) using 2 threads I/O nice 255 2022-08-11 10:04:42.261101 [info] main dispatch thread id from application: 0100 (World) is: 7fcab8e85700 TID: 15665 2022-08-11 10:04:42.261164 [info] shutdown thread id from application: 0100 (World) is: 7fcab8684700 TID: 15666 2022-08-11 10:04:42.262063 [info] Watchdog is disabled! 2022-08-11 10:04:42.262699 [info] io thread id from application: 0100 (World) is: 7fcabbae66c0 TID: 15664 2022-08-11 10:04:42.262755 [info] io thread id from application: 0100 (World) is: 7fcab7682700 TID: 15668 2022-08-11 10:04:42.263730 [info] vSomeIP 3.1.20.3 | (default) 2022-08-11 10:04:42.264287 [info] Network interface "lo" state changed: up
请注意:
让我们详细讨论一些问题。
0x0001
。service-example
。init()
的最后输出是 Application(World, 0100) is initialized (11, 100)
。最后的两个数字表示,如果回调阻塞超过 100ms 则 vsomeip 使用的调度器的最大数量为 11。这些参数可以配置。到目前为止,应用程序并没有做太多的工作,客户端与服务端之间没有区别。现在假设我们的 service-example
是服务端,我们想要写一个希望使用该服务的客户端。在第一步中,我们必须触发应用程序以提供服务实例,这可以通过在第一个示例中添加 offer_service 命令来实现:
服务示例 service-example.cpp
#include <vsomeip/vsomeip.hpp>
#define SAMPLE_SERVICE_ID 0x1234
#define SAMPLE_INSTANCE_ID 0x5678
std::shared_ptr< vsomeip::application > app;
int main() {
app = vsomeip::runtime::get()->create_application("World");
app->init();
app->offer_service(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID);
app->start();
}
在下一步,我们编写一个应用程序检查正在运行 “World” 的应用程序是否可用。考虑以下 client-example
代码,该代码创建了一个名为 Hello
的应用程序:
client-example.cpp
#include <iomanip> #include <iostream> #include <vsomeip/vsomeip.hpp> #define SAMPLE_SERVICE_ID 0x1234 #define SAMPLE_INSTANCE_ID 0x5678 std::shared_ptr< vsomeip::application > app; void on_availability(vsomeip::service_t _service, vsomeip::instance_t _instance, bool _is_available) { std::cout << "Service [" << std::setw(4) << std::setfill('0') << std::hex << _service << "." << _instance << "] is " << (_is_available ? "available." : "NOT available.") << std::endl; } int main() { app = vsomeip::runtime::get()->create_application("Hello"); app->init(); app->register_availability_handler(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, on_availability); app->request_service(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID); app->start(); }
为了尽可能简单,我们省略了所有可能的检查,例如注册是否成功。作为客户端,你必须告诉 vsomeip 你想要使用该服务,并且你需要注册回调,以便在服务可用时获得调用。客户端输出现在应该类似于:
Service [1234.5678] is NOT available.
2022-08-11 17:33:32.691125 [info] ON_AVAILABLE(0101): [1234.5678:0.0]
Service [1234.5678] is available.
当 app->start()
启动 vsomeip 事件循环时将调用可用性回调。
在服务侧应该有以下额外的输出:
2022-08-11 17:33:26.374865 [info] OFFER(0100): [1234.5678:0.0] (true)
从一个通用的 vsomeip 应用程序开始,我们创建了一个服务,该服务提供服务接口的实例,和一个希望使用该接口的客户端。下一步是在服务端实现一个可由客户端调用的函数。
服务示例必须准备好接收信息;这可以通过注册消息处理函数来完成。请查看以下代码:
带有服务提供和消息处理的 service-example.cpp
#include <iomanip> #include <iostream> #include <sstream> #include <vsomeip/vsomeip.hpp> #define SAMPLE_SERVICE_ID 0x1234 #define SAMPLE_INSTANCE_ID 0x5678 #define SAMPLE_METHOD_ID 0x0421 std::shared_ptr<vsomeip::application> app; void on_message(const std::shared_ptr<vsomeip::message> &_request) { std::shared_ptr<vsomeip::payload> its_payload = _request->get_payload(); vsomeip::length_t l = its_payload->get_length(); // Get payload std::stringstream ss; for (vsomeip::length_t i=0; i<l; i++) { ss << std::setw(2) << std::setfill('0') << std::hex << (int)*(its_payload->get_data()+i) << " "; } std::cout << "SERVICE: Received message with Client/Session [" << std::setw(4) << std::setfill('0') << std::hex << _request->get_client() << "/" << std::setw(4) << std::setfill('0') << std::hex << _request->get_session() << "] " << ss.str() << std::endl; // Create response std::shared_ptr<vsomeip::message> its_response = vsomeip::runtime::get()->create_response(_request); its_payload = vsomeip::runtime::get()->create_payload(); std::vector<vsomeip::byte_t> its_payload_data; for (int i=9; i>=0; i--) { its_payload_data.push_back(i % 256); } its_payload->set_data(its_payload_data); its_response->set_payload(its_payload); app->send(its_response); } int main() { app = vsomeip::runtime::get()->create_application("World"); app->init(); app->register_message_handler(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_METHOD_ID, on_message); app->offer_service(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID); app->start(); }
在客户端它有点复杂:
带有消息处理和发送函数的 client-example.cpp
#include <iomanip> #include <iostream> #include <sstream> #include <condition_variable> #include <thread> #include <vsomeip/vsomeip.hpp> #define SAMPLE_SERVICE_ID 0x1234 #define SAMPLE_INSTANCE_ID 0x5678 #define SAMPLE_METHOD_ID 0x0421 std::shared_ptr< vsomeip::application > app; std::mutex mutex; std::condition_variable condition; void run() { std::unique_lock<std::mutex> its_lock(mutex); condition.wait(its_lock); std::shared_ptr< vsomeip::message > request; request = vsomeip::runtime::get()->create_request(); request->set_service(SAMPLE_SERVICE_ID); request->set_instance(SAMPLE_INSTANCE_ID); request->set_method(SAMPLE_METHOD_ID); std::shared_ptr< vsomeip::payload > its_payload = vsomeip::runtime::get()->create_payload(); std::vector< vsomeip::byte_t > its_payload_data; for (vsomeip::byte_t i=0; i<10; i++) { its_payload_data.push_back(i % 256); } its_payload->set_data(its_payload_data); request->set_payload(its_payload); app->send(request); } void on_message(const std::shared_ptr<vsomeip::message> &_response) { std::shared_ptr<vsomeip::payload> its_payload = _response->get_payload(); vsomeip::length_t l = its_payload->get_length(); // Get payload std::stringstream ss; for (vsomeip::length_t i=0; i<l; i++) { ss << std::setw(2) << std::setfill('0') << std::hex << (int)*(its_payload->get_data()+i) << " "; } std::cout << "CLIENT: Received message with Client/Session [" << std::setw(4) << std::setfill('0') << std::hex << _response->get_client() << "/" << std::setw(4) << std::setfill('0') << std::hex << _response->get_session() << "] " << ss.str() << std::endl; } void on_availability(vsomeip::service_t _service, vsomeip::instance_t _instance, bool _is_available) { std::cout << "CLIENT: Service [" << std::setw(4) << std::setfill('0') << std::hex << _service << "." << _instance << "] is " << (_is_available ? "available." : "NOT available.") << std::endl; condition.notify_one(); } int main() { app = vsomeip::runtime::get()->create_application("Hello"); app->init(); app->register_availability_handler(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, on_availability); app->request_service(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID); app->register_message_handler(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_METHOD_ID, on_message); std::thread sender(run); app->start(); }
与服务端一样,我们需要注册一个消息处理函数来接收调用的响应。原则上创建发送消息是非常容易的(请求)。只需通过调用 create_request()
来获取请求对象,设置服务 ID、实例 ID 和方法 ID,最后将你的序列化数据写入 payload。在这里的示例中,我们将 0 到 9 的值写入 payload (std::vector< vsomeip::byte_t >
)。
当我们试图从客户端向服务端发送请求时,我们遇到了一个小问题。必须先启动应用程序(app->start()
)然后才能发送消息,因为我们需要一个正在运行的事件循环来处理消息。但是 app->start()
方法不返回,因为它内部有运行事件循环。所以,在调用 app->send(request, true)
之前,我们启动一个线程(run
)并在此线程中等待可用性回调的返回。
现在你应该得到的输出(我首先启动了服务):
2022-08-13 11:41:17.817276 [info] Client [100] is connecting to [101] at /tmp/vsomeip-101
2022-08-13 11:41:17.818534 [info] REGISTERED_ACK(0101)
SERVICE: Received message with Client/Session [0101/0001] 00 01 02 03 04 05 06 07 08 09
2022-08-13 11:41:17.913784 [info] REQUEST(0101): [1234.5678:255.4294967295]
CLIENT: Received message with Client/Session [0101/0001] 09 08 07 06 05 04 03 02 01 00
CLIENT: Service [1234.5678] is available.
2022-08-13 11:41:17.915125 [info] ON_AVAILABLE(0101): [1234.5678:0.0]
到目前为止,我们已经创建了一个实现方法的服务和一个调用该方法的客户端。但这并不是所有的可能,SOME/IP 规范还描述了事件处理。这意味着应用程序可以发送事件,如是订阅者感兴趣,他们可以订阅这些事件。通过定义 setter 和 getter 方法,可以实现提供属性的服务。为了不太复杂,我们在示例中删除了方法调用的实现,并实现了事件处理。首先让我们看看服务。
译者:在文件头部定义 EventID 及 EventGroupID 的值:
#define SAMPLE_EVENT_ID 0x8001
#define SAMPLE_EVENTGROUP_ID 0x0001
请在主函数中添加以下行:
const vsomeip::byte_t its_data[] = { 0x10 };
payload = vsomeip::runtime::get()->create_payload();
payload->set_data(its_data, sizeof(its_data));
std::set<vsomeip::eventgroup_t> its_groups;
its_groups.insert(SAMPLE_EVENTGROUP_ID);
app->offer_event(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_EVENT_ID, its_groups);
app->notify(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_EVENT_ID, payload);
请注意:
在客户端实现以下功能(为了更好地理解,我省略了前面讨论的所有内容):
... void run() { std::unique_lock<std::mutex> its_lock(mutex); condition.wait(its_lock); std::set<vsomeip::eventgroup_t> its_groups; its_groups.insert(SAMPLE_EVENTGROUP_ID); app->request_event(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_EVENT_ID, its_groups); app->subscribe(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, SAMPLE_EVENTGROUP_ID); } void on_message(const std::shared_ptr<vsomeip::message> &_response) { std::stringstream its_message; its_message << "CLIENT: received a notification for event [" << std::setw(4) << std::setfill('0') << std::hex << _response->get_service() << "." << std::setw(4) << std::setfill('0') << std::hex << _response->get_instance() << "." << std::setw(4) << std::setfill('0') << std::hex << _response->get_method() << "] to Client/Session [" << std::setw(4) << std::setfill('0') << std::hex << _response->get_client() << "/" << std::setw(4) << std::setfill('0') << std::hex << _response->get_session() << "] = "; std::shared_ptr<vsomeip::payload> its_payload = _response->get_payload(); its_message << "(" << std::dec << its_payload->get_length() << ") "; for (uint32_t i = 0; i < its_payload->get_length(); ++i) its_message << std::hex << std::setw(2) << std::setfill('0') << (int) its_payload->get_data()[i] << " "; std::cout << its_message.str() << std::endl; } ... int main() { app = vsomeip::runtime::get()->create_application("Hello"); app->init(); app->register_availability_handler(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID, on_availability); app->request_service(SAMPLE_SERVICE_ID, SAMPLE_INSTANCE_ID); app->register_message_handler(vsomeip::ANY_SERVICE, vsomeip::ANY_INSTANCE, vsomeip::ANY_METHOD, on_message); std::thread sender(run); app->start(); }
你可以看到,实现没有任何困难:
在控制台中,你现在应该看到以下的行输出:
2022-08-14 11:46:43.086133 [info] REGISTER EVENT(0100): [1234.5678.8001:is_provider=true]
2022-08-14 11:46:51.983577 [info] REGISTER EVENT(0101): [1234.5678.8001:is_provider=0:reliability=ff]
...
2022-08-14 11:46:51.079410 [info] SUBSCRIBE(0101): [1234.5678.0001:ffff:0]
2022-08-14 11:46:51.080129 [info] SUBSCRIBE ACK(0100): [1234.5678.0001.ffff]
圆括号中的数字同样是客户端 ID;我首先启动了服务,因此服务从自动配置中得到了 0100 号,客户端得到了 0101 号。
SOME/IP 尚未发明用于一个设备内的进程间通信(如 D-Bus),但已发明用于多个设备之间基于 IP 的通信。如果你想使用迄今为止为两个设备之间的通信开发的示例,无需更改 C++ 代码;但是你必须编写 vsomeip 配置文件。有关详细信息请参阅 vsomeip 用户指南;在这里,我们只讨论让系统运行的要点。
首先,我将省略一些关于 vsomeip 配置的介绍性文字。
/etc/vsomeip
。对于下面的配置示例,我假设服务在地址为 172.17.0.2
的设备上运行,而客户端的地址为 172.17.0.1
。
首先,让我们看一下服务配置的示例。
{ "unicast" : "172.17.0.2", "logging" : { "level" : "debug", "console" : "true", "file" : { "enable" : "false", "path" : "/tmp/vsomeip.log" }, "dlt" : "false" }, "applications" : [ { "name" : "World", "id" : "0x1212" } ], "services" : [ { "service" : "0x1234", "instance" : "0x5678", "unreliable" : "30509" } ], "routing" : "World", "service-discovery" : { "enable" : "true", "multicast" : "224.224.224.245", "port" : "30490", "protocol" : "udp", "initial_delay_min" : "10", "initial_delay_max" : "100", "repetitions_base_delay" : "200", "repetitions_max" : "3", "ttl" : "3", "cyclic_offer_delay" : "2000", "request_response_delay" : "1500" } }
对于通过 IP 的通信,单播(unicast)地址是强制的。让我们讨论其他条目:
create_application(<name>)
创建的)定义一个固定的客户端 ID,而不是由自动配置来确定。这将帮助你以后在跟踪中识别应用程序。这里必须设置客户端 ID,因为客户端 ID 在你的网络中必须是唯一的。如果不设置 clientID,自动配置将在每个设备上计算 clientID 1,通信将无法工作。注意: 确保你的设备已配置为接收多播消息(例如,通过 route add -nv 224.224.224.245 dev eth0
或类似;这取决于以太网设备的名称)。
考虑客户端的以下配置:
{ "unicast" : "172.17.0.1", "logging" : { "level" : "debug", "console" : "true", "file" : { "enable" : "false", "path" : "/var/log/vsomeip.log" }, "dlt" : "false" }, "applications" : [ { "name" : "Hello", "id" : "0x1313" } ], "routing" : "Hello", "service-discovery" : { "enable" : "true", "multicast" : "224.224.224.245", "port" : "30490", "protocol" : "udp", "initial_delay_min" : "10", "initial_delay_max" : "100", "repetitions_base_delay" : "200", "repetitions_max" : "3", "ttl" : "3", "cyclic_offer_delay" : "2000", "request_response_delay" : "1500" } }
由于客户端不提供服务,因此不需要 “services” 设置。
欢迎关注我的公众号:飞翔的小黄鸭
也许会发现不一样的风景
△ \triangle △ 译:SOME/IP 技术细节
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。