RCF--RPC(遠端呼叫框架)

jindezhang發表於2020-11-19

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. 對於自定義型別,需要是現實的兩個步驟是:註冊和序列化

相關文章