RPC(远程调用框架)
一、 RPC定义
RPC(Remote Procedure Call Protocol)——远程过程调用协议,是一种通过网络从远程计算机请求服务,就像调用本地方法一样,不需要了解底层网络技术的协议。RPC跨越了传输层和应用层,很容易开发分布式应用。
RPC框架通常包括五个部分:
-
User
-
User-stub
-
RPCRuntime
-
Server-stub
-
Server
这 5 个部分的关系如下图所示
User发起一个远程调用,它实际是通过本地调用调User-stub。User-stub 负责将调用的接口、方法和参数通过约定的协议规范进行编码并通过本地的 RPCRuntime 实例传输到远端的实例。远端 RPCRuntime 实例收到请求后交给 Server-stub 进行解码后,发起本地调用Server服务,调用结果再返回给User 端。
二、常用的RPC框架
目前常见的RPC框架:
gRPC
gRPC是一个高性能、通用的开源RPC框架,主要面向移动应用开发并基于HTTP/2协议标准而设计,基于ProtoBuf(Protocol Buffers)序列化协议开发,且支持众多开发语言(java,C++,go, objective-c,python,ruby,php,node,C#)。
Thrift
thrift是一个软件框架,用来进行可扩展且跨语言的服务的开发。它结合了功能强大的软件堆栈和代码生成引擎,以构建在 C++, Java, Python, PHP, Ruby, Erlang, C#, JavaScript, Node.js等编程语言间无缝结合的、高效的服务。
Dubbo
Dubbo是一个分布式服务框架(java),以及SOA治理方案。其功能主要包括:高性能NIO通讯及多协议集成,服务动态寻址与路由,软负载均衡与容错,依赖分析与降级等。
RCF
远程调用框架(Remote Call Framework, RCF)是一个可跨平台的IPC/RPC通信框架,使用C++编写。RCF并未使用独立的接口定义语言(IDL),而是直接采用C++定义RCF接口。
RCF框架功能:
· 支持单向和双向消息。
· 支持批量单向消息。
· 支持发布/订阅风格消息。
· 支持UDP上的多播和广播。
. 支持通过HTTP和HTTPS进行隧道传输。
· 支持多种传输方式 (TCP、UDP、Windows named pipes、UNIX local domain sockets)。
. 支持传输层信息压缩(Zlib)与加密(Kerberos、NTLM、Schannel、OpenSSL)。
. 支持异步远程调用。
. 支持双向连接,用于服务器到客户端的消息传递。
. 支持IPv4与IPv6。
· 健壮的版本支持。
· 内置序列化框架。
· 内建文件传输功能。
· 支持ProtoBuf。
RCF特点:
C++编写,我们要编写进程间通信的C ++组件,可以无缝整合。
简单,编译和使用简单。RCF采用C ++代码描述接口,不需要单独编写和编译IDL文件。构建更简单,开发更灵活。
可移植,RCF采用标准C ++编写,支持多种编译器、平台。
可伸缩性强,可以根据平台选择高效网络实现(IOCP on Windows, epoll on Linux, /dev/poll on Solaris, kqueue on FreeBSD)。从进程IPC到大型分布式系统都适用。
高效,序列化(内置、protobuf)方式比XML及JSON等方式更具效率。另外,在一些关键路径上使用了零拷贝(远程调用参数或数据不会进行内部复制, RCF::ByteBuffer类型数据序列化/反序列化不会拷贝)、零堆内存分配(使用相同的参数进行两次远程调用,则在同一连接上,RCF不会为第二次调用分配堆内存)。
RCF支持多种传输机制、线程模型以及多种消息传递方式(单向/双向,单向批量、发布/订阅,请求/响应)、异步调用与调度、传输层压缩、加密认证。
RCF比较稳定,成熟。RCF自2007年发布,已被大规模商用,主要用户:爱立信、西门子、惠普等公司。
三、RCF 编译
RCF以源代码形式提供(http://www.deltavsoft.com),RCF2.2和RCF3.0版本可以下载。下载的源码包:
RCF3.0只能在支持C++11以及C++14的编译器上编译。与RCF2.2相比,RCF3.0不再支持JSON-RPC、不用Boost.Serialization进行序列化、重新实现文件传输、采用proxy endpoints实现server to client通信。
RCF没有强制依赖第三方库,zlib、OpenSSL和 ProtoBuf依赖是可选。RCF可以直接编译到应用中,也可以编译成静态或动态库、然后链接到应用中。RCF在编译时,可以根据需要开启或关闭某些功能
Feature define | RCF feature | Default value |
---|---|---|
RCF_FEATURE_SSPI | Win32 SSPI-based encryption (NTLM, Kerberos, Schannel) | 1 on Windows. 0 otherwise. |
RCF_FEATURE_FILETRANSFER | File transfers(C++17) | 0 |
RCF_FEATURE_SERVER | Non-critical server-side functionality (server-to-client pingbacks, server objects, session timeouts) | 1 |
RCF_FEATURE_PROXYENDPOINT | Proxy endpoints | 1 |
RCF_FEATURE_PUBSUB | Publish/subscribe | 1 |
RCF_FEATURE_TCP | TCP transports | 1 |
RCF_FEATURE_UDP | UDP transports . | 1 |
RCF_FEATURE_NAMEDPIPE | Win32 named pipe transports | 1 on Windows. 0 otherwise. |
RCF_FEATURE_LOCALSOCKET | Unix local socket transports | 0 on Windows. 1 otherwise. |
RCF_FEATURE_HTTP | HTTP/HTTPS transports | 1 |
RCF_FEATURE_IPV6 | IPv6 support | 1 |
RCF_FEATURE_SF | RCF internal serialization | 1 |
RCF_FEATURE_LEGACY | Backwards compatibility with RCF 1.x and earlier | 0 |
RCF_FEATURE_ZLIB | Zlib-based compression. | 0 |
RCF_FEATURE_OPENSSL | OpenSSL-based SSL encryption. | 0 |
RCF_FEATURE_PROTOBUF | Protocol Buffer support. | 0 |
3.1 RCF编译成静态库
设置编译静态库的编译开关,设置include目录<rcf_distro>/include,添加RCF.cpp,编译生成RCF静态库。
3.2 RCF动态库编译
设置编译动态库的编译开关,设置define RCF_BUILD_DLL=1,设置include目录<rcf_distro>/include,添加RCF.cpp编译生成RCF动态库。
3.3 RCF直接编译至应用程序
VS编译方法:
新建空的项目,设置include目录<rcf_distro>/include。将<rcf_distro>/src/RCF/RCF.cpp添加至项目中,编写用户源码,然后进行编译。
g++编译方法:
g++ user-defined.cpp <rcf_distro>/src/RCF/RCF.cpp -I<rcf_distro>/include -lpthread -ldl -o user-defined
四、基于RCF框架编程(如何利用RCF框架编写远程调用)
4.1 RCF框架
RCF框架传输层采用Asio库实现,Asio是一个成熟的高性能后端C++网络库。RCF客户端默认是单线程同步模型,也支持异步调用。RCF服务端默认单线程同步调度,也支持多线程(线程池)、异步调度。
4.2 接口
RCF中的远程调用基于接口,RCF中的接口使用RCF_BEGIN()、 RCF_METHOD_() 以及RCF_END() 宏来标识。
RCF接口简单示例:
RCF_BEGIN(I_Calculator, “Calculate”)
RCF_METHOD_R2(int, Add, int, int); //----> int Add(int,int)
RCF_METHOD_V3(void, Sub, int, int, int&); //void Sub(int,int,int&)
RCF_END(I_Calculator)
RCF_BEGIN()宏格式:RCF_BEGIN(compile_time_name, runtime_name), runtime_name可以省略,省略时默认为compile_time_name。
RCF_METHOD_()宏的名称取决于方法的参数数量,以及方法是否具有返回类型。命名约定如下:
RCF_METHOD_{V|R}{}() - 定义RCF方法返回void(V)或非void®,并获取n参数,n范围从0到15。例如,RCF_METHOD_V0()定义一个void返回类型、无参数的函数。
RCF方法返回类型和参数类型可以为任意C++数据类型,但是类型必须存在serialize()函数。RCF方法的参数类型可以为值类型、指针、引用(const, 非const),而远程调用返回类型可以值类型或者非const引用。
RCF_END() 宏格式:RCF_END(compile_time_name)
宏展开相当于定义了RcfClient< compile_time_name> 类, 以及类方法。
RCF接口中的每个方法都有一个唯一方法ID,第一个方法ID是0,后续方法ID为前一个方法ID加1。单个接口中能定义的方法数量有限,默认情况下,最大数量为100个,可以调整RCF_MAX_METHOD_COUNT的值进行修改,但是最大允许值为200。
RCF接口需在远程调用客户端与服务端同时进行定义,接口定义顺序保持一致。客户端直接使用接口进行远程调用,服务端需实现接口全部函数定义。
4.2 客户端 —> 服务端
4.2.1 客户端编写步骤
- RcfClient对象实例化
定义RCF接口之后就可以实例化RcfClient<>。RcfClient<>对象通过Endpoint类型参数进行构造,指定传输协议。RCF::Endpoint类型包括RCF::TcpEndpoint、RCF::UdpEndpoint、RCF::HttpEndpoint、RCF::HttpsEndpoint、RCF::Win32NamedPipeEndpoint、RCF::UnixLocalEndpoint。
RcfClient<I_Calculator> client((RCF::TcpEndpoint(server_ip, port)));
一个RcfClient<>对象代表一个连接,同一时刻只能由一个线程使用,执行远程调用时自动与服务端建立连接,可以使用RCF::ClientStub::connect()创建连接。当RcfClient<>对象销毁,连接关闭。
RcfClient<>对象可以拷贝,但是RcfClient<>对象的每个拷贝都会与服务端建立新的连接。
RcfClient<I_Calculator> client1(( RCF::TcpEndpoint(port) ));
RcfClient<I_Calculator> client2(client1);
RcfClient<I_Calculator> client3;
client3 = client1;
- RcfClient对象属性设置(可选)
远程调用的客户端通过RCF::ClientStub进行控制,每一个RcfClient<>实例都包含一个RCF::ClientStub,可以调用RcfClient<>的getClientStub()方法获取。
RCF::ClientStub & clientStub = client.getClientStub();
通过RCF::ClientStub::setConnectTimeoutMs()设置连接超时:
client.getClientStub().setConnectTimeoutMs(2000);
也可以调用RCF::globals()::setDefaultConnectTimeoutMs()为所有RcfClient<>实例设置默认建立连接超时时间。
通过RCF::ClientStub::setRemoteCallTimeoutMs()设置远程调用超时:
client.getClientStub().setRemoteCallTimeoutMs(4000);
也可以调用RCF::globals().setDefaultRemoteCallTimeoutMs()为所有RcfClient<>设置默认远程调用超时时间。
通常远程调用,传输的数据都包含在远程调用中,但是可以通过设置远程调用用户定义数据域传输额外信息。可以通过RCF::ClientStub::setRequestUserData(string )传输额外请求,通过RCF::ClientStub::getResponseUserData()接收额外响应。
client.getClientStub().setRequestUserData( “e6a9bb54-da25-102b-9a03-2db401e887ec” );
client.Add(1,2);
std::string customReponseData = client.getClientStub().getResponseUserData();
std::cout << "Custom response data: " << customReponseData << std::endl;
- 执行远程调用
与执行本地调用一样,客户端通过RcfClient<>实例对象执行远程调用。如果调用服务端未定义的接口,将会抛出异常。
客户端执行远程调用有同步调用和异步调用,同步调用会阻塞当前线程直至远程调用结果返回。
client.Add(1, 2);
xxxx
在RCF中使用RCF::Future<>类实现异步调用。如果RCF::Future<>对象作为远程调用的返回值或参数,则是异步远程调用。
RCF::Future fRet = client.Add(1, 2);
xxxx
执行上述远程调用将会立即返回,启动异步远程调用的线程可以使用轮询判断调用是否完成:
while (!fRet.ready())
{
RCF::sleepMs(500);
}
或者等待调用完成:
fRet.wait();
一旦调用完成,可以从Future<>实例中获取返回值:
int charsPrinted = *fRet;
如果调用遇到错误,Future<>实例解引用时将会报错,也可以调用 RCF::Future<>::getAsyncException()查看发生的错误:
std::unique_ptrRCF::Exception ePtr = fRet.getAsyncException();
除了轮询或等待,启动调用的线程也可以提供完成时回调函数。当调用完成时,RCF将在后台线程上调用该回调函数。
typedef std::shared_ptr< RcfClient<I_Calculator> > CalculateServicePtr;
void onCallCompleted(CalculateServicePtr client, RCF::Future<int> fRet)
{
std::unique_ptr<RCF::Exception> ePtr = fRet.getAsyncException();
if (ePtr.get())
{
// Deal with any exception.
}
else
{
int charsPrinted = *fRet;
}
}
RCF::Future<int> fRet;
PrintServicePtr client( new RcfClient<I_Calculatore>(RCF::TcpEndpoint(port)) );
auto onCompletion = [=]() { onCallCompleted(client, fRet); };
fRet = client->Add( RCF::AsyncTwoway(onCompletion), 1, 2);
注意,单个连接上并发执行多个未完成的异步调用将会抛异常(Exception: multiple concurrent calls attempted on the same RcfClient<> object. To make concurrent calls, use multiple RcfClient<> objects instead.)。并发异步远程调用需要使用不同的RcfClient<>对象。
获取远程调用的返回值,继续后面处理
4.2.2 服务端编写步骤
- 配置RcfServer
RcfServer实例化方法与RcfClient类似。RcfServer可以设置一个或多个传输,而客户端只能有一个传输。另外,RcfServer对象不可拷贝与赋值。
如果只使用单个传输,则利用以RCF::Endpoint为参数的构造函数进行构造:
RCF::RcfServer server( RCF::TcpEndpoint(50001) );
也可以使用 RCF::RcfServer::addEndpoint()配置多个传输:
RCF::RcfServer server;
server.addEndpoint( RCF::TcpEndpoint(5001) );
server.addEndpoint( RCF::UdpEndpoint(50002) );
如果需配置传输相关的内容,可通过RCF::RcfServer::addEndpoint()的返回值进行设置:
RCF::RcfServer server;
//设置服务端最大并发连接数(只对于TCP)
server.addEndpoint(RCF::TcpEndpoint(5001)).setConnectionLimit(100);
//设置最大报文接受长度
server.addEndpoint(RCF::TcpEndpoint(5001)).setMaxIncomingMessageLength(1024*1024);
- RcfServer多线程设置(可选)
默认情况下,RcfServer采用单线程调度远程调用,调用RCF::RcfServer::setThreadPool()为RcfServer设置多线程。调用RCF::ThreadPool()方法可以分配固定数量线程的线程池,也可以配置数量随服务器负载动态变化的线程池。
1). 配置固定数目线程的线程池:
// Thread pool with a fixed number of threads (5).
RCF::ThreadPoolPtr threadPoolPtr( new RCF::ThreadPool(5) );
server.setThreadPool(threadPoolPtr);
2). 指定范围的线程池:
RCF::ThreadPoolPtr threadPoolPtr( new RCF::ThreadPool(1, 5) );
server.setThreadPool(threadPoolPtr);
默认情况下,为RcfServer对象分配的线程池会被所有传输共享。同时可以通过RCF::ServerTransport::setThreadPool()为指定传输分配线程池。
RCF::ThreadPoolPtr tcpThreadPool( new RCF::ThreadPool(1,5) );
RCF::ServerTransport & tcpTransport = server.addEndpoint(RCF::TcpEndpoint(50001));
tcpTransport.setThreadPool(tcpThreadPool);
RCF::ThreadPoolPtr pipeThreadPool( new RCF::ThreadPool(1) );
RCF::ServerTransport & pipeTransport = server.addEndpoint(
RCF::Win32NamedPipeEndpoint(“SvrPipe”));
pipeTransport.setThreadPool(pipeThreadPool);
- 远程调用接口实现
服务端需实现接口中定义的方法。实现方式有同步和异步,与服务端调度远程调用密切相关。
同步方法实现:
class CalculatorService
{
public:
int Add(int a, int b)
{
return a+b;
}
void Sub(int a, int b, int& re)
{
re = a-b;
}
};
异步方法实现:
class CalculatorService
{
public:
typedef RCF::RemoteCallContext<int, int, int> AddContext;
typedef RCF::RemoteCallContext<int, int, int, int&> SubContext;
int Add(int a, int b)
{
// Capture current remote call context.
AddContext addContext(RCF::getCurrentRcfSession());
// Start a new thread to dispatch the remote call.
std::thread addAsyncThread([=]() { addAsync(addContext); });
addAsyncThread.detach();
return 0;
}
void addAsync(AddContext addContext)
{
//获取参数
int & a = addContext.parameters().a1.get();
int & b = addContext.parameters().a2.get();
//设置输出参数(或返回值)
addContext.parameters().r.set( a+b );
//发送响应
addContext.commit();
}
void Sub(int a, int b, int&re)
{
// Capture current remote call context.
SubContextsubContext(RCF::getCurrentRcfSession());
// Start a new thread to dispatch the remote call.
std::thread subAsyncThread([=]() { subAsync(addContext); });
subAsyncThread.detach();
}
void subAsync(SubContext subContext)
{
//获取参数
int & a = subContext.parameters().a1.get();
int & b = subContext.parameters().a2.get();
//设置输出参数(或返回值)
subContext.parameters().a3.set( a-b );
subContext.commit();
}
};
当Add()/Sub()函数返回时,RcfServer不会向客户端发送响应。只有RCF::RemoteCallContext<>::commit()被调用时才会发送响应。
RCF::RemoteCallContext::parameters()提供访问远程调用所有参数的方法,包括返回值。
- 添加服务绑定
使用RCF::RcfServer::bind<>()创建服务绑定,每个服务绑定通过绑定名进行标识,默认的绑定名是接口的运行时名。
// creates a servant binding with the servant binding name "I_Calculator"
CalculatorService calculatorService;
server.bind<I_Calculator>(calculatorService);
也可以显式的设置服务绑定名:
server.bind<I_Calculator>(calculatorService, "Custom");
RcfClient<>实例可显式指定服务绑定名,默认是是接口的运行时名。
RcfClient<I_Calculator> client( RCF::TcpEndpoint(50001), "Custom" );
RcfServer利用服务绑定名调度远程调用给服务对象的相关函数。
- 服务端启动与停止
服务端只有启动之后才能调度远程调用,服务端调用RCF::RcfServer::start()启动。
server.start();
服务端启动之后,自动调度远程调用。RCF通常会在同一个服务器线程中调度远程调用,该线程从传输中读取远程调用请求然后调用同步方法执行、返回。RCF也支持异步调度,异步调度允许将远程调用转移到其他线程(调用异步方法实现),释放RCF服务器调度线程以继续调度其他远程调用。
服务端调用RCF::RcfServer::stop()停止。当RcfServer超出其作用域,服务自动停止。
server.stop();
- 服务发现设置(可选)
在很多情况下,服务器的端口是动态分配的。通过UDP广播或多播的方式将server的Port告知client。
// Interface for broadcasting port number of a TCP server.
RCF_BEGIN(I_Broadcast, "I_Broadcast")
RCF_METHOD_V1(void, ServerIsRunningOnPort, int)
RCF_END(I_Broadcast)
// Implementation class for receiving I_Broadcast messages.
class BroadcastImpl
{
public:
BroadcastImpl() : mPort()
{}
void ServerIsRunningOnPort(int port)
{
mPort = port;
}
int mPort;
};
// A server thread runs this function, to broadcast the server location once per second.
void broadcastThread(int port, const std::string &multicastIp, int multicastPort)
{
RcfClient<I_Broadcast> client(
RCF::UdpEndpoint(multicastIp, multicastPort) );
client.getClientStub().setRemoteCallMode(RCF::Oneway);
// Broadcast 1 message per second.
while (true)
{
client.ServerIsRunningOnPort(port);
RCF::sleepMs(1000);
}
}
// ***** Server side ****
// Start a server on a dynamically assigned port.
CalculatorService calculatorService;
RCF::RcfServer server( RCF::TcpEndpoint(0));
server.bind<I_Calculator>(calculatorService);
server.start();
//端口自动选择才能调用
int port = server.getIpServerTransport().getPort();
// Start broadcasting the port number.
RCF::ThreadPtr broadcastThreadPtr(new RCF::Thread(
[=]() { broadcastThread(port, "232.5.5.5", 50001); }));
// ***** Client side ****
// Clients will listen for the broadcasts before doing anything else.
RCF::UdpEndpoint udpEndpoint("0.0.0.0", 50001);
udpEndpoint.listenOnMulticast("232.5.5.5");
RCF::RcfServer clientSideBroadcastListener(udpEndpoint);
BroadcastImpl broadcastImpl;
clientSideBroadcastListener.bind<I_Broadcast>(broadcastImpl);
clientSideBroadcastListener.start();
// Wait for a broadcast message.
while (!broadcastImpl.mPort)
{
RCF::sleepMs(1000);
}
// Once the clients know the port number, they can connect.
RcfClient<I_Calculator> client( RCF::TcpEndpoint(server_ip, broadcastImpl.mPort));
client.Add(1,2);
4.3 服务端 —> 客户端
如果服务端需要主动与客户端通信,需通过客户端充当通信服务端、服务端充当通信客户端。RCF提供代理端点(proxy endpoint)的方式实现,代理端点只能采用TCP传输方式。
4.3.1 配置代理服务器
代理服务器接收通信服务器的连接,并将连接保存至连接池中。使用RCF::RcfServer实例化代理服务器对象,然后调用RCF::RcfServer::setEnableProxyEndpoints(true)。一旦启动,RcfServer将开始注册通信服务器并代理从客户端到那些目标服务器的连接。
// 代理服务器。
int proxyPort = 50001;
RCF :: RcfServer proxyServer(RCF::TcpEndpoint(“0.0.0.0”,proxyPort));
proxyServer.setEnableProxyEndpoints(true);
proxyServer.start();
4.3.2 配置通信服务器
每个通信服务器必须向代理服务器提供一个服务器名,名称可以任意(多个相同服务器名的通信服务器只能有一个与proxy服务器连接)。通信服务器RcfServer使用 RCF::ProxyEndpoint参数进行构造,RCF::ProxyEndpoint指定代理服务器的IP和port,以及服务器的名称。
// 通信服务器.
RCF::RcfServer destinationServer(RCF::ProxyEndpoint(RCF::TcpEndpoint(proxyIp, proxyPort), “RoamingPrintSvr”) );
PrintService printService;
destinationServer.bind<I_PrintService>(printService);
destinationServer.start();
启动之后,通信服务器将开始启动到代理服务器的连接。
4.3.3 服务实现
在通信服务端需实现定义的接口,提供远程服务。实现方法与前面相同。
4.3.4 通信客户端配置
通信服务器和代理服务器启动并运行后,客户端使用RCF::ProxyEndpoint并指定代理服务器以及通信服务器的名称连接到通信服务器,然后执行远程调用。
// Client calling through proxy server.
RcfClient<I_PrintService> client( RCF::ProxyEndpoint(proxyServer, “RoamingPrintSvr”) );
client.Print(“Calling I_PrintService through a proxy endpoint”);
客户端现在可以像往常一样执行远程调用,可以断开连接并重新连接,就像未使用代理连接。
4.4 发布/订阅模式
RCF内置支持发布/订阅模式,当发布者发布特定主题的消息时,所有订阅此主题的订阅者都会收到这个消息。订阅者需保证能与发布者建立连接,发布者不会主动与订阅者建立连接。
4.4.1 发布者
可以使用RCF::RcfServer::createPublisher<>()创建发布者,函数返回RCF::Publisher<>对象,RCF::Publisher<>对象用于发布远程调用。如果创建函数不带参数将会创建带默认主题的发布者,默认主题是RCF接口运行时名字。例如:
RCF::RcfServer pubServer( RCF::TcpEndpoint(50001) );
pubServer.start();
typedef RCF::Publisher<I_PrintService> PrintServicePublisher;
typedef std::shared_ptr< PrintServicePublisher > PrintServicePublisherPtr;
//创建发布者(主题名: I_PrintService)
PrintServicePublisherPtr publisherPtr = pubServer.createPublisher<I_PrintService>();
也可以显式指定主题名,例如可以在同一个RCF接口创建两个不同主题的发布者。
RCF::PublisherParms pubParms;
pubParms.setTopicName(“Topic_1”);
PrintServicePublisherPtr publisher1Ptr = pubServer.createPublisher<I_PrintService>
(pubParms);
pubParms.setTopicName(“Topic_2”);
PrintServicePublisherPtr publisher2Ptr = pubServer.createPublisher<I_PrintService>
(pubParms);
使用 RCF::Publisher<>::publish()发布调用:
publisherPtr->publish().Print(“First published message.”);
发布的远程调用是单向的,会被所有订阅了当前主题的所有订阅者接收。
当RCF::Publisher<>对象被销毁或调用RCF::Publisher<>::close(),发布主题被关闭、所有与订阅者的连接都会断开。
4.4.2 订阅者
订阅一个发布使用RCF::RcfServer::createSubscription<>()。与发布者类似,可以订阅默认主题或者显式指定订阅主题。
RCF::RcfServer subServer( RCF::TcpEndpoint(-1) );
subServer.start();
PrintService printService;
RCF::SubscriptionParms subParms;
subParms.setPublisherEndpoint( RCF::TcpEndpoint(50001) );
subParms.setTopicName( “Topic_1” );
subParms.setOnSubscriptionDisconnect(&onSubscriptionDisconnected);
RCF::SubscriptionPtr subscriptionPtr = subServer.createSubscription<I_PrintService>(
printService, subParms);
当订阅对象销毁或调用Subscription::close(),将会终止订阅。
4.5 文件传输
RCF内置支持文件传输,RCF文件传输功能默认禁用的。如需使用RCF文件传输功能,编译时需开启,定义RCF_FEATURE_FILETRANSFER=1。
4.5.1 文件下载
客户端使用RCF::ClientStub::downloadFile()函数从RcfServer上下载文件,函数RCF::ClientStub::downloadFile()第一个参数是下载ID,下载ID由 RcfServer通过调用 RCF::RcfSession::configureDownload()进行分配。第二个参数是文件保存路径。第三参数是可选的RCF::FileTransferOptions类型,RCF::FileTransferOptions 可以设置文件下载相关参数,包括下载带宽设置、下载文件片段。
//client-side
//remote call
std::string downloadId = rcf_client->downloadFileId( fileName );
RCF::Path fileToDownloadTo = “C:\Users\RD373\Documents\client.txt”;
rcf_client->getClientStub().downloadFile( downloadId, fileToDownloadTo );
//server-side
std::string downloadFileId(std::string fileToDownload) {
std::string downloadId = RCF::getCurrentRcfSession().configureDownload(fileToDownload);
return downloadId;
}
4.5.2 文件上传
客户端使用RCF::ClientStub::uploadFile()上传文件到RcfServer。RCF::ClientStub::uploadFile()第一个参数是上传ID,由client调用 RCF::generateUuid()设置。上传ID也可以为空,为空时值由RcfServer填充返回。在服务端,必须在文件上传之前设置上传文件保存的目录,通过 RCF::RcfServer::setUploadDirectory()进行设置。
示例:
// Server-side code.
server.setUploadDirectory(“C:\MyApp\Uploads”);
server.start();
// Client-side code.
std::string uploadId = RCF::generateUuid();
RCF::Path fileToUpload = “C:\Document1.pdf”;
client.getClientStub().uploadFile(uploadId, fileToUpload);
//remote call
client.AddDocument(uploadId);
4.5.3 监控文件传输
在客户端,为监控文件的传输,可以使用RCF::FileTransferOptions参数,设置RCF::FileTransferOptions::mProgressCallback自定义回调函数,在文件传输完成之前,回调函数不停地被执行:
void clientTransferProgress(const RCF::FileTransferProgress& progress)
{
double percentComplete = (double)progress.mBytesTransferredSoFar / (double)progress.mBytesTotalToTransfer;
std::cout << "Download progress: " << percentComplete << “%” << std::endl;
}
RCF::FileTransferOptions transferOptions;
transferOptions.mProgressCallback = &clientTransferProgress;
client.getClientStub().downloadFile(downloadId, fileToDownloadTo, &transferOptions);
在服务端,可以使用 RCF::RcfServer::setDownloadProgressCallback() 和RCF::RcfServer::setUploadProgressCallback()注册函数,当一个块被下载或上传,回调函数均会执行。
4.5.4 传输带宽控制
RCF文件传输自动使用尽可能多的带宽进行文件传输,但允许用户对文件传输的带宽进行限制。
服务端可以使用 RCF::RcfServer::setUploadBandwidthLimit() 和RCF::RcfServer::setDownloadBandwidthLimit()控制上传和下载最大带宽,带宽被所有客户端共享,总带宽不能超过所设置的最大带宽。同时,也可以自定义带宽限制,支持更加精细的带宽控制。使用RCF::RcfServer::setUploadBandwidthQuotaCallback() 和RCF::RcfServer::setDownloadBandwidthQuotaCallback()进行设置。例如,服务端可以根据不同客户端设置不同的上传带宽:
// 1 Mbps quota bucket.
RCF::BandwidthQuotaPtr quota_1_Mbps( new RCF::BandwidthQuota(110001000/8) );
// 56 Kbps quota bucket.
RCF::BandwidthQuotaPtr quota_56_Kbps( new RCF::BandwidthQuota(56*1000/8) );
// Unlimited quota bucket.
RCF::BandwidthQuotaPtr quota_unlimited( new RCF::BandwidthQuota(0) );
RCF::BandwidthQuotaPtr uploadBandwidthQuotaCb(RCF::RcfSession & session)
{
// Use clients IP address to determine which quota to allocate from.
const RCF::RemoteAddress & clientAddr = session.getClientAddress();
const RCF::IpAddress & clientIpAddr = dynamic_cast<const RCF::IpAddress &>(clientAddr);
if ( clientIpAddr.matches( RCF::IpAddress(“192.168.0.0”, 16) ) )
{
return quota_1_Mbps;
}
else if ( clientIpAddr.matches( RCF::IpAddress(“15.146.0.0”, 16) ) )
{
return quota_56_Kbps;
}
else
{
return quota_unlimited;
}
}
// Assign a custom file upload bandwidth limit.
server.setUploadBandwidthQuotaCallback(&uploadBandwidthQuotaCb);
客户端同样可以设置单用户下载带宽限制以及多用户共享带宽限制。
使用RCF::FileTransferOptions::mBandwidthLimitBps配置单个客户端连接的带宽限制:
RCF::FileTransferOptions transferOptions;
transferOptions.mBandwidthLimitBps = 1024 * 1024; // 1 MB/sec
client.getClientStub().downloadFile(downloadId, fileToDownloadTo, &transferOptions);
使用RCF::FileTransferOptions::mBandwidthQuotaPtr 在多个客户端连接间共享限制配额:
RCF::BandwidthQuotaPtr clientQuotaPtr(new RCF::BandwidthQuota(1024*1024));
auto doUpload = [=](RcfClient<I_PrintService>& client)
{
std::string uploadId = RCF::generateUuid();
RCF::Path fileToUpload = “C:\Document1.pdf”;
RCF::FileTransferOptions transferOptions;
transferOptions.mBandwidthQuotaPtr = clientQuotaPtr;
client.getClientStub().uploadFile(uploadId, fileToUpload, &transferOptions);
};
std::vectorstd::thread clientThreads;
clientThreads.push_back(std::thread(std::bind(doUpload, std::ref(client1))));
clientThreads.push_back(std::thread(std::bind(doUpload, std::ref(client2))));
clientThreads.push_back(std::thread(std::bind(doUpload, std::ref(client3))));
for ( std::thread& thread : clientThreads )
{
thread.join();
}
4.6 日志
RCF提供可配置的日志系统,通过RCF::enableLogging() 和RCF::disableLogging()函数进行控制RCF框架的日志输出。如果需要使用日志功能,需要包含<RCF/Log.hpp>头文件。
RCF日志功能默认不可用,可调用 RCF::enableLogging()开启日志。RCF::enableLogging()有两个可选参数,分别是日志输出地和输出日志级别。
Log target Log output location
RCF::LogToDebugWindow() 在Visual Studio调试窗口中输出
RCF::LogToStdout() 输出到标准输出
RCF::LogToFile(const std::string & logFilePath) 日志输出到指定文件
RCF::LogToFunc(std::function<void(const RCF::ByteBuffer &)>) 日志输出给用户自定义函数
在Windows平台上,默认输出到RCF::LogToDebugWindow。在非Windows平台上,默认日志输出到RCF::LogToStdout。日志级别的范围从0(完全没有日志记录)到4(详细日志记录),默认日志级别为2。
如果要禁用日志记录,调用RCF::disableLogging()。
RCF::enableLogging()和RCF::disableLogging()是线程安全的。
示例:
// Using default values for log target and log level.
RCF::enableLogging();
// Using custom values for log target and log level.
int logLevel = 2;
RCF::enableLogging(RCF::LogToDebugWindow(), logLevel);
// Disable logging.
RCF::disableLogging();
也可以输出自定义日志:
std::string s = “Reversing a vector of strings…”;
std::string filePath = “C:\Users\log.txt”;
RCF::ByteBuffer byte_buff(s);
RCF::LogToFile logFile(filePath, true);
logFile.write(byte_buff);
4.7 版本控制
版本升级与兼容是必不可少。RCF提供强大的版本控制支持,允许自由升级组件,不会破坏与先前部署的组件的兼容性。RCF支持向后兼容(兼容至RCF2.0),新旧RCF版本可以自动协商。
1). 添加或删除方法
在RCF接口的开头或中间插入方法会更改现有方法ID,从而破坏与现有客户端和服务器的兼容性。为了保持兼容性,需要在RCF接口的末尾添加新方法:
//版本1
RCF_BEGIN(I_Calculator,“I_Calculator”)
RCF_METHOD_R2(double,add,double,double)
RCF_METHOD_R2(双,减,双,双)
RCF_END(I_Calculator)
//版本 2
RCF_BEGIN(I_Calculator, “I_Calculator”)
RCF_METHOD_R2(double, add, double, double)
RCF_METHOD_R2(double, subtract, double, double)
RCF_METHOD_R2(double, multiply, double, double)
RCF_END(I_Calculator)
只要在接口中留有占位符,就可以删除方法,以保留接口中其余方法的方法ID。
// Version 1
RCF_BEGIN(I_Calculator, “I_Calculator”)
RCF_METHOD_R2(double, add, double, double)
RCF_METHOD_R2(double, subtract, double, double)
RCF_END(I_Calculator)
// Version 2.
RCF_BEGIN(I_Calculator, “I_Calculator”)
RCF_METHOD_PLACEHOLDER()
RCF_METHOD_R2(double, subtract, double, double)
RCF_END(I_Calculator)
2). 添加或删除参数
添加或删除参数必须是RCF_METHOD_XX()方法的最后的参数,否则会破坏版本的兼容性。RCF服务器和客户端会忽略远程调用中传递的任何冗余参数,如果未提供预期参数,则默认初始化。
增加参数:
// Version 1
RCF_BEGIN(I_Calculator, “I_Calculator”)
RCF_METHOD_R2(double, add, double, double)
RCF_END(I_Calculator)
// Version 2
RCF_BEGIN(I_Calculator, “I_Calculator”)
RCF_METHOD_R3(double, add, double, double, double)
RCF_END(I_Calculator)
删除参数:
// Version 1
RCF_BEGIN(I_Calculator, “I_Calculator”)
RCF_METHOD_R2(double, add, double, double)
RCF_END(I_Calculator)
// Version 2
RCF_BEGIN(I_Calculator, “I_Calculator”)
RCF_METHOD_R1(double, add, double)
RCF_END(I_Calculator)
3). 重命名接口
RCF接口由其运行时名称标识,由RCF_BEGIN()宏的第二个参数中所指定。只要保留此名称,就可以更改接口的编译时名称,而不会破坏兼容性。
// Version 1
RCF_BEGIN(I_Calculator, “I_Calculator”)
RCF_METHOD_R2(double, add, double, double)
RCF_END(I_Calculator)
// Version 2
RCF_BEGIN(I_CalculatorService, “I_Calculator”)
RCF_METHOD_R2(double, add, double, double)
RCF_END(I_CalculatorService)
五、RCF性能和优缺点
测试了多个客户端在不同服务端模型(单线程同步、单线程异步、多线程同步、多线程异步)、不同传输方式(TCP传输、HTTP传输)下,远程调用的延时。测试发现,远程调用延时几ms到几十ms。单线程同步性能最差,多线程同步性能跟服务器线程数以及客户端数量有很大关系,单线程异步与多线程异步性能相当,表现最好。
TCP传输比HTTP传输远程调用延时少2+ms。
HTTP传输:采用HTTP长连接,RCF请求自动封装HTTP头部,Body部分仍然是byte类型的数据,携带远程调用绑定服务名、函数ID、函数参数等信息,通过Post方式发送。RCF响应正文格式与RCF请求Body部分类似。
TCP传输:传输的数据是byte类型,RCF请求数据格式、内容与HTTP传输Body部分完全一致。TCP响应内容与HTTP传输响应正文相同。
优势:
简单,不需要单独编写、编译IDL文件,开发更简单。
可移植,采用标准C ++编写,跨平台。
可伸缩性强,从父子进程IPC到大型分布式系统都可应用。
高效, 序列化方式比XML、JSON等方式更有效率。另外,在一些关键路径上使用了零拷贝、零堆内存分配。
RCF支持多种多种传输机制、线程模型以及多种消息传递方式(单向/双向,单向批量、发布/订阅,请求/响应)、异步、压缩、加密认证。
比较成熟、稳定。
缺点:
文档偏少
接口直接采用C++定义,没有单独IDL文件,不能跨语言。
不支持json、xml等其他序列化方式。
不支持负载均衡、容错。
六、Q&A
Q1 如果客户端正在进行远程调用,如何防止用户界面无响应?
A: 在非UI线程上运行远程调用,或使用进度回调以制定时间间隔重新绘制UI
Q2. 客户端如何取消长时间运行的远程调用?
A: 使用进度回调。配置回调时间间隔,取消调用(RCF::Rca_Cancel)时,回调函数会抛出异常(Remote call canceled by client)。
Q3: 如何在远程调用中终止服务器?
A: 不能在远程调用中直接调用RCF::RcfServer::stop()来终止,stop()调用将会等待所有工作线程退出,包括调用stop()的线程,从而导致死锁。
如果确实需要在远程调用中停止服务器,则可以启动新线程来执行此操作:
RCF_BEGIN(I_Echo, “I_Echo”)
RCF_METHOD_R1(std::string, Echo, const std::string &)
RCF_END(I_Echo)
class EchoImpl
{
public:
std::string Echo(const std::string &s)
{
if (s == “stop”)
{
// Spawn a temporary thread to stop the server.
RCF::RcfServer * pServer = & RCF::getCurrentRcfSession().getRcfServer();
RCF::Thread t( = { pServer->stop(); } );
t.detach();
}
return s;
}
};
Q4: 如何远程访问实现类私有函数
A: 可以将RcfClient<>作为服务实现类的友元.
RCF_BEGIN(I_Echo, “I_Echo”)
RCF_METHOD_R1(std::string, Echo, const std::string &)
RCF_END(I_Echo)
class EchoImpl
{
private:
friend RcfClient<I_Echo>;
std::string Echo(const std::string &s)
{
return s;
}
};
Q5: 多个RcfClient<>实例如何使用同一个TCP连接?
A: 可以使用RCF::getClientStub().releaseTransport()将Tcp连接从一个RcfClient<>实例转移到另一个RcfClient<>实例
cfClient<I_AnotherInterface> client2( client.getClientStub().releaseTransport() );
client2.AnotherPrint(“Hello World”);
client.getClientStub().setTransport( client2.getClientStub().releaseTransport() );
Q6: 如何强制断开服务器与客户端的连接?
A: 在服务端调用RCF::getCurrentRcfSession().disconnect()
Q7: 在服务端如何检测客户端断开连接?
A: 当客户端断开连接时,服务端上相关联的RCF::RcfSession对象将会销毁。可以调用RcfSession::setOnDestroyCallback()设置回调函数进行通知
void onClientDisconnect(RCF::RcfSession & session)
{
// …
}
class ServerImpl
{
void SomeFunc()
{
auto onDestroy = [&](RCF::RcfSession& session) { onClientDisconnect(session); };
RCF::getCurrentRcfSession().setOnDestroyCallback(onDestroy);
}
};
Q8:当发布者停止发布消息,订阅能否知道?
A: 可以使用断开连接回调通知
RCF::SubscriptionParms subParms;
subParms.setPublisherEndpoint( RCF::TcpEndpoint(50001) );
subParms.setOnSubscriptionDisconnect(&onSubscriptionDisconnected);
RCF::SubscriptionPtr subscriptionPtr = subServer.createSubscription<I_PrintService>(
printService, subParms);
或使用RCF::Subscription::isConnected()进行轮询连接。
Q9: 在RCF接口中可以使用指针?
A: 指针不能作为RCF接口中方法的返回类型,因为没有安全的编组方式。但是可以作为方法参数,但是推荐使用引用或智能指针。
学习连接
其他问题
编译失败:
- WIN32_LEAN_AND_MEAN;_WIN32_WINNT=0x0500;增加预处理定义
在Windows平台上,用来统计微秒级别耗时信息,需要用到两个Windows API:
BOOL WINAPI QueryPerformanceFrequency(
_Out_ LARGE_INTEGER *lpFrequency
);
BOOL WINAPI QueryPerformanceCounter(
_Out_ LARGE_INTEGER *lpPerformanceCount
);
• 使用自定义类型的时候,需要序列化
• 对于map和pair等类型,先去别名,再放到接口中。否则会提示参数不够
- 对于自定义类型,需要是现实的两个步骤是:注册和序列化