RCF--RPC(远程调用框架)

RPC(远程调用框架)

一、 RPC定义

RPC(Remote Procedure Call Protocol)——远程过程调用协议,是一种通过网络从远程计算机请求服务,就像调用本地方法一样,不需要了解底层网络技术的协议。RPC跨越了传输层和应用层,很容易开发分布式应用。

RPC框架通常包括五个部分:

  1. User

  2. User-stub

  3. RPCRuntime

  4. Server-stub

  5. 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 defineRCF featureDefault value
RCF_FEATURE_SSPIWin32 SSPI-based encryption (NTLM, Kerberos, Schannel)1 on Windows. 0 otherwise.
RCF_FEATURE_FILETRANSFERFile transfers(C++17)0
RCF_FEATURE_SERVERNon-critical server-side functionality (server-to-client pingbacks, server objects, session timeouts)1
RCF_FEATURE_PROXYENDPOINTProxy endpoints1
RCF_FEATURE_PUBSUBPublish/subscribe1
RCF_FEATURE_TCPTCP transports1
RCF_FEATURE_UDPUDP transports .1
RCF_FEATURE_NAMEDPIPEWin32 named pipe transports1 on Windows. 0 otherwise.
RCF_FEATURE_LOCALSOCKETUnix local socket transports0 on Windows. 1 otherwise.
RCF_FEATURE_HTTPHTTP/HTTPS transports1
RCF_FEATURE_IPV6IPv6 support1
RCF_FEATURE_SFRCF internal serialization1
RCF_FEATURE_LEGACYBackwards compatibility with RCF 1.x and earlier0
RCF_FEATURE_ZLIBZlib-based compression.0
RCF_FEATURE_OPENSSLOpenSSL-based SSL encryption.0
RCF_FEATURE_PROTOBUFProtocol 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 客户端编写步骤

  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;

  1. 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;

  1. 执行远程调用

与执行本地调用一样,客户端通过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 服务端编写步骤

  1. 配置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);

  1. 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);

  1. 远程调用接口实现

服务端需实现接口中定义的方法。实现方式有同步和异步,与服务端调度远程调用密切相关。

同步方法实现:

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()提供访问远程调用所有参数的方法,包括返回值。

  1. 添加服务绑定

使用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利用服务绑定名调度远程调用给服务对象的相关函数。

  1. 服务端启动与停止

服务端只有启动之后才能调度远程调用,服务端调用RCF::RcfServer::start()启动。

server.start();

服务端启动之后,自动调度远程调用。RCF通常会在同一个服务器线程中调度远程调用,该线程从传输中读取远程调用请求然后调用同步方法执行、返回。RCF也支持异步调度,异步调度允许将远程调用转移到其他线程(调用异步方法实现),释放RCF服务器调度线程以继续调度其他远程调用。

服务端调用RCF::RcfServer::stop()停止。当RcfServer超出其作用域,服务自动停止。

server.stop();

  1. 服务发现设置(可选)

在很多情况下,服务器的端口是动态分配的。通过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接口中方法的返回类型,因为没有安全的编组方式。但是可以作为方法参数,但是推荐使用引用或智能指针。

学习连接

RCF进程间通信Demo程序

C++高精度计时器——微秒级时间统计

其他问题

编译失败:

  1. 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等类型,先去别名,再放到接口中。否则会提示参数不够

  1. 对于自定义类型,需要是现实的两个步骤是:注册和序列化
  • 2
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值