电子说
我们已经可以基于vsomeip实现SOME/IP应用,并且服务端和客户端之间进行消息的通信,消息的内容称为Payload。
但是设想一下,如果当我们需要传递的消息内容是一个比较复杂的数据结构,比如一个结构体,一两个倒也没事,多了以后,Payload的打包、解析和联调都会是件麻烦的事。
这时,我们会想到序列化,比如用Google Protocol Buffer之类的,是不是可以解决问题呢?
对于非AUTOSAR设备之间的通信,是可以解决的,但对于与AUTOSAR设备之间的通信,恐怕就行不通了,因为Payload是需要遵循AUTOSAR规范的,如图:
于是,我们又会想到,如果有人能把序列化这一步也帮我们做好那就更好了,这就是RPC(Remote Procedure Call,远程过程调用)可以做到的事了。
GENIVI的CommonAPI C++是基于vsomeip实现的RPC框架,今天就让我们一起看一下它是怎么用的吧~
搭建CommonAPI的开发环境,有点费劲的,除了依赖于boost和vsomeip,还有CommonAPI和CommonAPI-SomeIP,以及C++代码生成工具,这里就不一一说明了,我已经整理好,放在Github上,关注公众号,回复“演示代码”,可以获得项目链接。
环境OK了以后,我们可以就创建第一个HelloWorld工程了,按照如图所示CommonAPI的工作流程:
其中,FrancaIDL是一种接口描述语言,和编程语言无关。fidl文件是用IDL写的,它描述了服务提供的接口信息,包括类型(比如method、broadcast、attribute等)、参数、返回值。
创建HelloWorld.fidl文件:
package commonapi
interface HelloWorld {
version {major 1 minor 0}
method sayHello {
in {
String name
}
out {
String message
}
}
}
fdepl文件描述了服务的部署信息,包括Service ID、Instance ID、Method ID、Event ID等。
创建HelloWorld.fdepl文件:
import "platform:/plugin/org.genivi.commonapi.someip/deployment/CommonAPI-SOMEIP_deployment_spec.fdepl"
import "HelloWorld.fidl"
define org.genivi.commonapi.someip.deployment for interface commonapi.HelloWorld {
SomeIpServiceID = 4660
method sayHello {
SomeIpMethodID = 123
}
}
define org.genivi.commonapi.someip.deployment for provider as MyService {
instance commonapi.HelloWorld {
InstanceId = "test"
SomeIpInstanceID = 22136
}
}
CommonAPI代码生成工具几乎支持Franca的全部功能。
用准备好的工具生成代码:
commonapi-core-generator-linux-x86_64 -sk ./fidl/HelloWorld.fidl
commonapi-someip-generator-linux-x86_64 ./fidl/HelloWorld.fdepl
在src-gen/v1/commonapi目录里,可以看到如下这些生成的代码文件:
万事俱备,可以开发应用程序咯~
对于服务端,主程序代码如下:
std::shared_ptr
其中,HelloWorldStubImpl是继承于工具生成的HelloWorldStubDefault:
class HelloWorldStubImpl: public v1_0::commonapi::HelloWorldStubDefault {
public:
HelloWorldStubImpl();
virtual ~HelloWorldStubImpl();
virtual void sayHello(const std::shared_ptr;
};
HelloWorldStubImpl实现了sayHello接口,正如fidl定义的,当客户端发送name,回复“Hello name !”:
void HelloWorldStubImpl::sayHello(const std::shared_ptr
{
std::stringstream messageStream;
messageStream << "Hello " << _name << "!";
std::cout << "sayHello('" << _name << "'): '" << messageStream.str() << "'\\n";
_reply(messageStream.str());
};
对于客户端,主程序如下:
std::shared_ptr < CommonAPI::Runtime > runtime = CommonAPI::Runtime::get();
std::shared_ptr
客户端不需要实现接口,直接使用工具生成的HelloWorldProxy就可以了。
编译运行的结果如下:
现在再看一下应该选择CommonAPI还是vsomeip呢?用vsomeip的话,依赖的东西少,Payload的打包和解析要自己写,工作量大,自由发挥的空间也大,用CommonAPI的话,依赖的东西多,环境搭建相对复杂,接口可以用IDL描述,这在SOA中非常有用,很多代码由工具生成,基本通信几乎不需要联调,主要的开发工作是实现服务的接口,相当于填充业务逻辑,工作量少,同时可以发挥的空间也小。很多事都是这样吧,获得便利的同时也会损失一些自由,如何选择还是要具体分析。
通过这个示例,我们看到使用RPC通信和上一篇中基于消息的通信是截然不同的编程体验,RPC让客户端可以像调用本地函数一样调用服务端的函数,很显然它们并不在同一个进程中,这是如何做到的呢?
下面,我们结合一张经典的RPC原理框图来看一下客户端的sayHello到底是怎么调到服务端的sayHello的:
1.client调用本地接口sayHello,HelloWorldProxy就是client的stub(桩),负责将sayHello的参数进行打包,组装成一个或者多个网络请求(这些取决于通信协议和序列化方式);
2.client的stub通过socket向server的stub,也叫skeleton(骨架),发送请求;
3.skeleton通过socket接收到请求;
4.请求消息被发送到skeleton,在这里就是HelloWorldStubDefault,负责将收到的请求拆包,取得client发送的参数;
5.HelloWorldStubDefault把参数发给了HelloWorldStubImpl的sayHello。
6.server在HelloWorldStubImpl的sayHello里处理了请求,通过sayHelloReply_t将返回值发给了HelloWorldStubDefault,它负责把返回值进行打包,组装成一个或者多个网络响应;
7.server的skeleton通过socket向client的stub发出响应;
8.stub通过socket接收到响应消息;
9.响应消息被发送到client的stub,也就是HelloWorldProxy,它负责将响应消息进行解析,取得server发送的参数;
10.client通过HelloWorldProxy的sayHello,得到了returnMessage。
至此,client完成了一次RPC调用~
可以看出,在RPC框架中,桩的实现原理是非常关键的,它屏蔽了网络通信的实现,让客户端可以像调用本地接口一样调用服务端提供的接口,而不用关心用的什么通信协议、序列化方式,以及所有的通信细节。
CommonAPI的桩是由代码生成器根据IDL生成的,而在有的RPC框架里,还可以用动态代理的方式得到。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !