工作需要接触以下vSOMEIP,这篇文章记录在探索过程中的经验
github上的vSOMEIP项目:https://github.com/GENIVI/vsomeip
名词解释
boost:一个可移植的C++库
项目READEME
vsomeip Overview
协议栈包含如下的的的东西
- 一个SOME/IP的共享库(libvsomeip3.so)
- 一个用于SOME/IP的服务发现库(libvsomeip3-sd.so),可以在运行时加载,如果使能了服务发现
vsomeip in 10 minutes
SOME/IP 介绍
SOME/IP是”Scalable service-Oriented middlewarE over IP“,这个中间件用用来为汽车设计的,他是兼容AUTOSAR的,相关的公开的介绍在http://some-ip.com/中,这个wiki不会深入解释其他中间件指定的原因,而是对SOME/IP做了整体的基本介绍和它的开元实现
下面是SOME/IP定义的三个主要的部分
- On-wire 格式
- 协议
- 服务发现
SOME/IP On-wire Format
原则上来说,SOME/IP通信由设备间或通过IP之上的订阅者之间发送的消息构成,看下面的图片
这里有两个设备(A和B),A设备发送一个SOME/IP报文给B ,并且得到一个返回报文,潜在的通信协议可以是TCP或UDP,对于消息本身是没有差别的,现在我们假设设备B正在运行一个服务,A可以通过这条Message调用这个服务,这个服务返回一条A发出的Message的响应
SOME/IP Message由两部分组成:头和数据段,上图中可以看到报文头中有如下主要的标识:
- Service ID:每个服务都有唯一的标识符
- Method ID:0 - 32767表示Method,32768 - 65535表示events
- Length:数据段的长度(包含下面的IDs,也就是多8个字节)
- Client ID:ECU内部的client的唯一的标识符,整车上是唯一的
- Session ID:会话的标识符,每次调用都会增加
- Protocol Version:0x01
- Interface Version:服务接口的主要版本
- Message Type:
- REQUEST(0x00):期待响应的请求
- REQUEST_NO_RETURN(0x01):无需响应的请求
- NOTIFICATION(0x02):一个notification/event的请求,不期待响应
- RESPONSE(0x80):响应报文
- Return Code:
- E_OK(0x00):没有错误发生
- E_NOT_OK(0x01):一个未定义的错误发生
- E_WRONG_INTERFACE_VERSION(ox08):接口版不匹配
- E_MALFORMED_MESSAGE(0x09):序列化失败
- E_WRONG_MESSAGE_TYPE(0x0A):受到了为定义的消息类型
我们看到有“REQUESTs”和“RESPONSEs”常规请求和客户端订阅的event的notification message,错误是常规的响应,不同的错误有不同的返回码
数据场包含序列化的数据,这张图展示了序列化的简单的形式
SOME/IP 协议
主要有2点比较重要:
- so-called传输绑定(UDP和TCP)
- 基本的传输模式:发布/订阅和请求/响应
上面说到了潜在的传输协议可以是UDP或者TCP,UDP的情况下,SOME/IP Message 不是片段的,可以有超过一个Messsage在一个UDP报文中,但是,不能呢个超过UDP 报文的长度(最高是1400Bytes),更大的message必须通过TCP传输,在这种情况下可以用到TCP的所有鲁棒的特性,如果在TCP中有传输故障出现,SOME/IP规范允许so-call magic cookies用来再次寻找下一条message的起始
需要注意的是服务接口必须是被实例化,因为可能有很多实例都有相同的接口,而且每个实例定义都有一个附加的标识符(instance ID),然而 instance ID不是报文头的一部分,instance ID是通过port口号来定义的,这意味着同一个接口的不同实例不可能有相同的port
下图展示了基本的SOME/IP通信模式
除了标准的REQUEST/RESPONSE机制可以被远程调用,还有PUBLISH/SUBSCRIBE的事件机制,需要注意的是在SOME/IP协议的event通常是在同一个group中的,因此只有可能订阅整个event group而不是订阅单一的event本身,SOME/IP规范也称“fields”,这种情况下setter/getter方法遵循R/R模式,并且通知message是周期发送的,订阅本身是通过SOME/IP的服务发现来完成的。
SOME/IP Service discovery
SOME/IP SD是给用在本地的服务实例上的,用来监测是否服务实例是否实现了发布/订阅的机制,主要通过so-called提供message来实现的,这意味着每个设备会广播message,message中会包含所有需要提供给该设备的服务,SOME/IP message是通过UDP来发送message的,如果服务被client应用所请求,但是这个时候没有提供,然后“find messages”可以被发送,其他的SOME/IP SD message可以在一个eventgoup中被发布或者订阅
下图展示了SOME/IP message 的整体结构
这些对于初学者涞水足够了,详细的可以在示例中看到
vsomeip short overview
在开始执行介绍的例子之前,先简单看看GENIVI实现的基本的结构
如图片中所显示的,SOME/IP通信不仅仅是设备间的通信(外部通信)还包括内部通信,两个设备通过Endpoints来通信(使用TCP或者UDP)还有端口号和其他的参数,所有这些参数通过vsomeip的配置文件来设置(json格式,看vsomeip的用户手册),内部的通信通过内部的socket来通信,使用Boost.Asio库,尽管内部通信不通过中央组件(比如D-Bus),它依然是很快的。
中央vsomeip组件管理器只有在受到外部设备发送的message后,然后它把message分散出去,每一个设备上只有一个组建管理器,如果第一次运行someip的时候什么也没有配置,它仍然会开启
vsomeip不会实现数据结构的序列化,这些是由SOME/IP绑定的的API来实现的,vsomeip仅仅包含SOME/IP协议和服务发现
这只是SOME/IP的一个非常短的介绍,但是对于一开始也够了
Preparation / Prerequisites
之前提到了vsomeip是需要boost.Asio库,所以确定年你已经装了BOOST的库,当boost成功的装好了以后,你可以像往常一样轻松的构建vsomeip
cd vsomeip
mkdir build
cmake ..
make
这会工作,但是如果想避免一些特别的问题,我建议在你的CMake文件中至少加上下面一个参数
/build$ cmake -DENABLE_SIGNAL_HANDLING=1 ..
这个参数确保你可以关闭你的vsomeip应用程序而且不会有问题(除非共享库内存空间 /dev/shm/vsomeip 在使用 Ctrl-C 停止应用程序的三时候没有正确的移除)
First Application
创建一个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();
}
这很简单,你已经创建了第一个应用对象,初始化它而且启动了他,init方法一定要在创建vsomeip应用之前调用,会执行下面的步骤来初始化它:
- 加载配置
- 检测路由配置并且初始化路由
- 安装信号句柄
开始方法在调用init之后执行用来开始message进程,接收message将会通过sockets和寄存器回调函数,用他们给用户应用
READY
现在创建一个CMake文件来构建这个应用,它看起来像这个样子
CMakeLists.txt (Example)
cmake_minimum_required (VERSION 2.8)
set (CMAKE_CXX_FLAGS "-g -std=c++0x")
find_package (vsomeip 2.6.0 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-sample.cpp)
target_link_libraries(service-example vsomeip ${Boost_LIBRARIES})
像往常一样继续(创建build路径,运行CMake并且build),然后开始它 (service-example),你应该会在console中得到输出
2017-03-20 10:38:20.885390 [info] Parsed vsomeip configuration in 0ms
2017-03-20 10:38:20.889637 [info] Default configuration module loaded.
2017-03-20 10:38:20.889797 [info] Initializing vsomeip application "World".
2017-03-20 10:38:20.890120 [info] SOME/IP client identifier configured. Using 0001 (was: 0000)
2017-03-20 10:38:20.890259 [info] No routing manager configured. Using auto-configuration.
2017-03-20 10:38:20.890367 [info] Instantiating routing manager [Host].
2017-03-20 10:38:20.890641 [info] init_routing_endpoint Routing endpoint at /tmp/vsomeip-0
2017-03-20 10:38:20.890894 [info] Client [1] is connecting to [0] at /tmp/vsomeip-0
2017-03-20 10:38:20.891039 [info] Service Discovery enabled. Trying to load module.
2017-03-20 10:38:20.891647 [info] Service Discovery module loaded.
2017-03-20 10:38:20.892045 [info] Application(World, 1001) is initialized (11, 100).
2017-03-20 10:38:20.892210 [info] Starting vsomeip application "World" using 2 threads
2017-03-20 10:38:20.892668 [info] Watchdog is disabled!
2017-03-20 10:38:20.893312 [info] Network interface "lo" is up and running.
2017-03-20 10:38:20.898471 [info] vSomeIP 2.6.2
2017-03-20 10:38:20.898708 [info] Sent READY to systemd watchdog
2017-03-20 10:38:20.898854 [info] SOME/IP routing ready.
需要注意的是:
- 这些步骤很类似于服务器和客户端,他们没有什么区别,仅仅是一个vsomeip应用
- 直到现在你不需要任何的配置文件
我们来说说一些细节的地方
- 首先你看到的配置文件已经被加载了,你不需要配置,因此你使用的是默认的配置
- 你不需要为你的应用配置一个client ID,因此这是一个vsomeip的特性,自动配置,寻找一个应用的client ID,第一个数字是 0x001
- 也不路由器也不需要配置,因此路由器是在vsomeip应用第一次启动的时候自动运行的,这个就是service-example
- 默认情况下服务发现是启用的,没有静态路由,这可能需要一些配置参数
- 最后的init()的输出是Application(world, 1) is initialized (11, 100)。这两个最后的数的意思是如果回调模块超过了100ms的话,最大的调度程序vsomip使的使用是11,这个参数是可以被配置的
- 默认情况下两个线程会创建,用来接收SOME/IP message,这允许vsomeip并行的操作长message
- 然后你会看到当前的vsomeip版本然后SOME/IP路由已经ready
Availability
到目前为止这个应用不会做更多的工作了,然后这和client和service没有差别,现在我们假设我们的 service-example 是service然后我们想去写一个client然后使用这个服务,第一步是我们出发这个应用来提供一个service实例,这些可以通过在我们的示例中添加一个offer_service命令
service-example.cpp with offer
#include <vsomeip/vsomeip.hpp>
#define SAMPLE_SERVICE_ID 0x1234
#define SAMPLE_INSTANCE_ID 0x5678
std::shard_ptr< vsomeip::application > app
int main() {
app = vsompip::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_availablility(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.")
}
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();
}
为了保持尽可能的简单,我们省略了所有的必要的检查,比如注册是否成功,作为client,你需要告诉告诉vsomeip你向使用这个服务并且你需要注册一个回调函数,为了在服务可用的时候得到一个对话,client的输出类似这样
Service [1234.5678] is NOT available.
2017-03-21 04:14:37.720313 [info] REQUEST(0002): [1234.5678:255.4294967295]
Service [1234.5678] is available.
当vsomeip事件循环通过app->start()开始的时候,可用的回调被调用
在service端会有下面的行
2017-03-21 04:14:33.850964 [info] OFFER(0001): [1234.5678:0.0]
Requset/Responce
从一个普通的vsomeip应用程序开始,我们创建了一个service,这个service提供了一个service接口的实例,然后一个client想要使用这个接口,现在下一步是在service端实现一个函数,这个函数可以被client调用
这个service例子是用来准备接收一个message,可以通过接收一个message句柄,请看下面的代码
service-example.cpp 和message句柄
#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, true);
}
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端会比较复杂一点
#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, true);
}
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();
}
Subscribe / Notify
Communication between two devices
SOME/IP不是为内部通信设计的,而是在多个设备之间的基于IP的通信,如果你想使用这个例子使用C++开发两个设备之间的通信,可以不用修改这些C++ 代码,只需要修改下面的vsopeip配置文件,详细的请看vsomeip的使用手册,这里仅仅讨论在系统运行过程中最主要的部分
我将首先忽略关于vsomeip配置的整体介绍文字
- 这个协议栈是由多个json文件来配置的
- 标准的json文件是 /etc/vsomeip
- 有可能通过设置环境变量VSOMEIP_CONFIGURATION来改变这个路径或者定义一个单独的配置文件
- 有可能需要复制配置文件到可执行文件的目录中
详细的配置例子我假设service运行的设备地址是172.17.0.2,client是172.17.0.1
我们先看service的配置文件
{
"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 addrest是强制必须的,我们来看看其他的的部分
- logging:这个设置是可选的,设置"console"=true来使log message在console
- applications:你可以为每一个应用定义(你通过create_application(< name >)),一个不定的client ID替代它,它是由autoconfiguration来确定的,它将在后面帮助你在后面的跟踪中识别你的应用程序,需要强制性的设置client ID,因为client ID在你的网络中必须是独一无二的,如果你没有设置client ID,自动配置文件将会在每个设备上计算client ID 1,并且通讯也不会正确运行
- service:对于每个service实例,其端口号必须是可以使用的,如果这个端口号是“unreliable”的,这可以是一个UDP端口,如果是一个"reliable",那么将会使用TCP来传输
- routing:每一个设备只有一个路由管理器,这个路由管理器将会被附在第一个vsomeip应用程序上
- service-discovery:所有的参数仅仅在service-disconvery是能的时候才有意义,在这种情况下强制的参数是广播地址,用来发送服务发现报文,端口号还有协议,另一个参数确定了这个message被发送的频率,延时参数等,请看SOME/IP的用户手册
- 确保你的设备已经配置了广播地质(224.224.224.245 dev eth0 或者类似的,这取决于你的eth设备的名称)
下面看client端
{
"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"
}
}