DDS通信中间件——RPC(请求响应模式)
做了十年DDS通信中间件产品的程序员和大家分享一下对DDS这套规范的个人理解。预期本系列文章将包括以下内容陆续更新:
- DDS规范概述
- DCPS规范解读 & QoS策略
- XTypes规范解读
- RTPS规范解读
- DDS安全规范解读
- DDS-RPC规范解读( 请求响应模式 & 函数调用模式 )
- DDS-TSN规范解读
- DDS-XRCE规范解读
1. 概述
1.1. RPC是什么?
RPC是Remote Procedure Call(远程过程调用)的缩写,RPC是一种通信模式,常见的通信模式如下图所示,RPC区别于其他通信模式的特点在于:RPC是双向通信,即通信双方均需要发送和接收数据,图中的请求响应模式和远程过程调用在广义上都可以归类为RPC通信模式,两者的主要区别在于:
- 请求响应模式还是比较低层次的数据的概念,请求端发送请求数据、响应端发送响应数据,不抽象“功能”,优点是实现简单,适用于需要接收端发送响应的场景,缺点是使用起来比较繁琐,例如下面的计算服务Calculater,一种方法是将每个方法看成一个服务,需要定义包含方法入参的结构体作为请求数据,定义包含返回值类型成员的结构提作为响应数据,另一种方式是将Calculater作为一个服务,定义联合体并根据不同的方法名称定义不同关联的数据类型来定义请求类型以及响应类型,这两种方式在包含多个服务或者服务包含的方法非常复杂的场景下需要定义的数据结构会非常复杂;
- 远程函数调用模式是比较高层次的抽象,不使用“数据”的概念来描述请求或者响应,而用函数(功能、方法)的形式来描述服务,请求端调用某个函数(功能),响应端实现函数(功能)返回值,常见的此类通信模式的产品包括:gRPC、SOME/IP、Corba、HTTP。同样以下面定义的Calculater服务作为示例,客户端只需要根据业务调用不同的方法即可,底层方法名称、参数、返回值的封装都自动生成。
// 计算服务
interface Calculater
{
// 加法计算
int add(long a, long b);
// 减法计算
double sub(double a, double b, double c);
// 平均值计算
double avg(long datas[]);
};
// 请求响应模式需要定义的数据结构
union CalculaterRequest switch(kind)
{
case ADD:
long a;
long b;
case SUB:
double a;
double b;
double c;
case AVG:
sequence<long> datas;
};
union CaluculaterResponse switch(kind)
{
case ADD:
long ret;
case SUB:
double ret;
case AVG:
double ret;
};
1.2. RPC有什么好处?
RPC的使用场景非常广泛,包括:
- 发送方发送数据后需要接收端确认的场景;
- 服务化场景,在系统中将某个特定的功能集中到某一个应用(组件),其他需要该功能的组件通过RPC来请求,这种架构可以:
- 降低对系统资源的需求,当实现这个功能需要很高的系统资源时,这种服务化架构可以只让服务端部署在高配置的环境,而客户端仅需要支持RPC通信即可,降低了客户端对系统资源的依赖。
- 简化系统的设计,服务化架构使得系统的设计变得清晰,组件与组件之间的功能松耦合。
1.3. 使用DDS实现RPC有什么好处?
- DDS的主题数据通过IDL来描述,而IDL同样适合于需要平台无关的语言来描述服务(接口、函数),两者可以使用相同的建模语言;
- DDS本身已经是将系统中的数据按照主题的方式逻辑化进行应用与硬件的解耦,非常适合同样需要将系统功能“服务化”的需求,并且DDS的自动发现机制可以轻易的解决服务定位的需求;
- 使用DDS实现RPC可以将DDS本身的优良的特性带给RPC通信模式,包括:
- 异构屏蔽,支持异构平台(操作系统、通信协议)以及异构编程语言的特性;
- QoS的配置,支持在RPC时配置各种不同的QoS;
- 安全性,可以复用底层DDS的安全策略进行:身份验证、访问控制以及数据加密等安全性功能;
- 互操作性,由于标准规范的存在,替换不同厂家的RPC开发库或者DDS库的代价极低;
- 实时性高,基于DDS通信的实时性得到保证;
- 使用DDS实现RPC使得一个项目里面仅需要采用一种技术即可完成多种通信模式,降低项目依赖以及复杂度;
2. DDS-RPC规范
DDS-RPC规范中定义了请求响应模式以及函数调用两种模式的RPC接口,本篇文章主要介绍第一种请求响应模式,函数调用模式在后续的文章中介绍。
2.1. 概念与架构
DDS-RPC的通信架构图参见下图,通信实体包括:
- 服务端实体,服务端实现服务(例如:上面定义的Calculater服务中的加法、减法、求平均的逻辑),并通过响应收到的请求对外提供服务。在底层通信实现层面,服务端关联一个数据读者(关联请求主题)用于接收请求以及一个数据写者(关联响应主题)用于发送响应数据。
- 客户端实体,客户端构造请求并接收响应完成服务的请求,在底层通信实现层面,客户端关联一个数据写者(关联请求主题)用于发送请求以及一个数据读者(关联响应主题)用于接收响应数据。
2.2. 服务定义
请求响应模式的服务定义包括:
- 服务名称,字符串类型,在代码接口中通过设置请求者以及响应者的参数指定;
- 请求数据类型定义,通过IDL描述,扩展@RPCRequestType标记表示该类型为请求类型,如下的
ReqType
; - 响应数据类型定义,通过IDL描述,扩展@RPCReplyType标记表示该类型为响应类型,如下的
ResType
。
@RPCRequestType
struct ReqType
{
long a;
long b;
};
@RPCReplyType
struct ResType
{
long ret;
};
2.3. 主题映射
底层的两个主题名要么是用户指定要么是根据服务名自动构造,也有两种形式:
- 在IDL中使用标记来指定;
- 在代码中通过设置请求者以及响应者的参数指定。
<topic_name> ::= <interface_name>"_"<service_name>"_"[ "Request" | "Reply" ]
| <user_def_alpha_num>
<service_name> ::= "Service"
| <user_def_alpha_num>
<user_def_alpha_num> ::= ^[[:alnum:]_ ]+$
2.4. 类型映射
请求响应的类型映射非常简单,除了定义数据类型以及增加标注之外几乎没有额外的工作。但是在实现层面需要实现“请求与响应的关联”,即需要明确响应数据是针对哪一个请求数据,因为服务可能会同时收到多个客户端发送的请求。基于这个需求,DDS-RPC会在标注了请求结构的结构体前面自动加上请求头,响应结构同理会自动加上响应头。请求头与响应头的定义及其说明参见下面的代码。
这里需要特别解释一下服务实例的配置,首先多个应用可以提供相同的服务(反过来看就是相同的服务可能会存在多个实例),那么在客户端请求时可以选择向其中的某个实例进行请求,这时候就需要在服务启动时赋予服务唯一的实例标识,在DDS-RPC中我们使用字符串来进行标识。
// 请求数据的唯一标识
struct SampleIdentity
{
// 使用请求端的数据写者的唯一标识来标识请求端
GUID_t writer_guid;
// 序列号,用于唯一标识相同请求端的数据
SequenceNumber_t sequence_number;
};
// 自动添加的请求头
struct RequestHeader
{
// 唯一标识请求数据
SampleIndentity_t requestId;
// 服务实例名称,用于指明提供多个相同服务的服务端的唯一标识
InstanceName instanceName;
};
// 响应头
struct ReplyHeader
{
// 关联的请求数据
SampleIdentity relatedRequestId;
// 枚举类型的返回值,例如:请求成功、失败等通用的状态
RemoteExceptionCode_t remoteEx;
};
2.5. 请求响应模式接口
协议中是以Modern C++的语法来定义的,对于不熟悉的同学来看会比较难理解,这里我们以Traditional C++的语法进行描述请求响应提供了哪些接口。
2.5.1. 请求者实体参数(RequesterParams)
配置 | 说明 | 是否必须 |
---|---|---|
域号 | 在指定的DDS域内进行RPC通信 | 否 |
域参与者 | 允许传入已经创建好的域参与者,用于复用,默认自动创建新的域参与者 | 否 |
服务名称 | 该请求者请求的服务名称 | 是 |
请求主题名 | 自定义请求主题名,默认根据服务按照规则自动构造 | 否 |
响应主题名 | 自定义响应主题名,默认根据服务按照规则自动构造 | 否 |
数据写者QoS | 指定发送请求所需的服务质量,默认为持久化1的可靠传输 | 否 |
数据读者QoS | 指定接收响应所需的服务质量,默认为持久化1的可靠传输 | 否 |
2.5.2. 请求者实体模板(Requester<>)
模板参数:请求类型以及响应类型。
方法 | 说明 |
---|---|
构造/析构系统方法 | 以指定的参数构造请求者以及销毁请求者实体 |
send_request系列方法 | 发送请求,包括指定服务实例名称 |
receive_reply系列方法 | 等待并接收响应,包括指定超时时间、响应的个数 |
wait_for_replies系列方法 | 等待响应数据,包括指定超时时间、响应的个数 |
take_reply系列方法 | 接收响应,包括指定请求、响应的个数 |
set_listener | 设置异步回调接收响应的接口 |
2.5.3. 响应者实体参数(ReplierParams)
配置 | 说明 | 是否必须 |
---|---|---|
域号 | 在指定的DDS域内进行RPC通信 | 否 |
域参与者 | 允许传入已经创建好的域参与者,用于复用,默认自动创建新的域参与者 | 否 |
服务名称 | 该响应者提供的服务名称 | 是 |
服务实例 | 该响应者的服务实例 | 否 |
请求主题名 | 自定义请求主题名,默认根据服务按照规则自动构造 | 否 |
响应主题名 | 自定义响应主题名,默认根据服务按照规则自动构造 | 否 |
数据写者QoS | 指定发送响应所需的服务质量,默认为持久化1的可靠传输 | 否 |
数据读者QoS | 指定接收请求所需的服务质量,默认为持久化1的可靠传输 | 否 |
2.5.4. 响应者实体模板(Replier<>)
模板参数:请求类型以及响应类型。
方法 | 说明 |
---|---|
构造/析构系统方法 | 以指定的参数构造请求者以及销毁响应者实体 |
send_reply系列方法 | 发送响应,包括指定请求标识 |
receive_request系列方法 | 等待并接收请求,包括指定超时时间、请求的个数 |
wait_for_request系列方法 | 等待请求数据,包括指定超时时间、请求的个数 |
take_request系列方法 | 接收请求,包括响应的个数 |
set_listener | 设置异步回调接收请求的接口 |
3. 请求响应模式代码示例
以下示例还是以ZRDDS为例。
3.1. 服务定义
在IDL中定义以下内容,包括请求类型以及响应类型。
@RPCRequestType
struct RequestType
{
long longVal;
string strVal;
};
@RPCReplyType
struct ReplyType
{
boolean boolVal ;
long arrayVal[20];
};
3.2. 支撑代码生成
使用支持DDS-RPC的IDL编译器编译IDL,生成支撑代码,将生成的代码添加到工程中备用。
3.3. 请求端代码
3.3.1. 声明请求者实体
新建RPCRRRequester.h
并使用ZRDDSRPCRequester
宏声明请求实体,其中第一个参数为请求类型,第二个参数为响应类型,该宏将声明RequestTypeRequester
类给用户使用。
#ifndef RPCRRRequester_h__
#define RPCRRRequester_h__
#include "RPCRRExample.h"
#include "RPCRRExampleDataWriter.h"
#include "RPCRRExampleDataReader.h"
#include "RPCRRExampleTypeSupport.h"
#include "ZRDDSRPCRequester.h"
ZRDDSRPCRequester(RequestType, ReplyType);
#endif // RPCRRRequester_h__
3.3.2. 实现请求者实体
新建RPCRRRequester.cpp
对RPCRRRequester.h
中声明的函数进行实现,采用以下方式:
#include "RPCRRRequester.h"
#include "DomainParticipantFactory.h"
#define TReq RequestType
#define TRep ReplyType
#include "ZRDDSRPCRequester.cpp"
#undef TRep
#undef TReq
3.3.3. 主函数
#include "RPCRRRequester.h"
int main(int argc, char* argv[])
{
// 1. 配置参数
RequesterParams reqParam;
reqParam.domain_id(150);
reqParam.service_name("RPCRRExample");
// 2. 使用参数创建请求实体
// 创建同步请求者
RequestTypeRequester* syncRequester = new RequestTypeRequester(reqParam, NULL);
// 3. 发送请求
RequestType reqSample;
RequestTypeInitialize(&reqSample);
reqSample.longVal = 9;
// 向所有服务端发送
ReturnCode_t retCode = syncRequester->send_request(reqSample);
if (retCode != RETCODE_OK)
{
printf("send request failed.\n");
return -1;
}
// 指定服务端发送
retCode = syncRequester->send_request(reqSample, "instance-1");
if (retCode != RETCODE_OK)
{
printf("send request failed.\n");
return -2;
}
// 4. 同步模式获取响应
// 在指定时间内等待响应
Duration_t timeout = DurationFromSec(3);
retCode = syncRequester->wait_for_replies(timeout);
if (retCode != RETCODE_OK)
{
printf("wait request failed.\n");
return -3;
}
// 获取相应数据
ReplyTypeSeq repSampleSeq;
retCode = syncRequester->take_replies(repSampleSeq);
if (retCode != RETCODE_OK)
{
printf("take replies failed.\n");
return -4;
}
// 处理获取到的响应数据
for (unsigned int i = 0; i < repSampleSeq.length(); ++i)
{
printf("received reply %u\n", repSampleSeq[i].boolVal);
}
// 6. 清理实体
// 清理同步实体
syncRequester->destroy();
delete syncRequester;
return 0;
}
3.4. 响应端代码
3.4.1. 声明响应实体
新建RPCRRReplier.h并使用ZRDDSRPCReplier宏声明请求实体,其中第一个参数为请求类型,第二个参数为响应类型,该宏将声明ReplierTypeReplier类给用户使用。
#ifndef RPCRRReplier_h__
#define RPCRRReplier_h__
#include "RPCRRExample.h"
#include "RPCRRExampleDataWriter.h"
#include "RPCRRExampleDataReader.h"
#include "RPCRRExampleTypeSupport.h"
#include "ZRDDSRPCReplier.h"
#include "WaitSet.h"
ZRDDSRPCReplier(RequestType, ReplyType);
#endif // RPCRRReplier_h__
3.4.2. 实现响应实体
新建RPCRRReplier.cpp对RPCRRReplier.h中声明的函数进行实现,采用以下方式:
#include "RPCRRReplier.h"
#include "DomainParticipantFactory.h"
#define TReq RequestType
#define TRep ReplyType
#include "ZRDDSRPCReplier.cpp"
#undef TRep
#undef TReq
3.4.3. 主函数
#include "RPCRRReplier.h"
class myReplierListener : public ZRDDSRPCEntityListener
{
public:
virtual void on_request_received(ZRDDSRPCEntity* replierEntity)
{
ReplyTypeReplier* replier = dynamic_cast<ReplyTypeReplier*>(replierEntity);
// 获取相应数据
RequestTypeSeq reqSampleSeq;
ReturnCode_t retCode = replier->take_requests(reqSampleSeq);
if (retCode != RETCODE_OK)
{
printf("take requests failed.\n");
return;
}
// 处理获取到的请求数据
for (unsigned int i = 0; i < reqSampleSeq.length(); ++i)
{
printf("received request %u\n", reqSampleSeq[i].longVal);
// 4. 发送响应
ReplyType reqSample;
ReplyTypeInitialize(&reqSample);
reqSample.boolVal = true;
// 手动设置requestId成员
reqSample.header.requestId = reqSampleSeq[i].header.requestId;
// 响应指定请求
retCode = replier->send_reply(reqSample);
if (retCode != RETCODE_OK)
{
printf("send repliy failed.\n");
return;
}
}
}
};
int main(int argc, char* argv[])
{
// 1. 配置参数
ReplierParams repParam;
repParam.domain_id(150);
repParam.service_name("RPCRRExample");
repParam.instance_name("instance-0");
// 2. 使用参数创建请求实体
// 创建异步响应者实体
myReplierListener* listener = new myReplierListener();
repParam.instance_name("instance-1");
ReplyTypeReplier* asyncReplier = new ReplyTypeReplier(repParam, listener);
// 5. 异步模式获取请求
// 在listener中处理
getchar();
// 6. 清理实体
// 清理异步实体
asyncReplier->set_listener(NULL);
asyncReplier->destroy();
delete asyncReplier;
return 0;
}