Following programming language traditions, let's start with a "Hello World" example. We'll write a client that sends strings to a server, and a server that prints the strings to standard output.
Here is the server:
#include <RCF/RCF.hpp> #include <iostream> RCF_BEGIN(I_HelloWorld, "I_HelloWorld") RCF_METHOD_V1(void, Print, const std::string &) RCF_END(I_HelloWorld) class HelloWorldImpl { public: void Print(const std::string & s) { std::cout << "I_HelloWorld service: " << s << std::endl; } }; int main() { RCF::RcfInitDeinit rcfInit; HelloWorldImpl helloWorld; RCF::RcfServer server( RCF::TcpEndpoint(50001) ); server.bind<I_HelloWorld>(helloWorld); server.start(); std::cout << "Press Enter to exit..." << std::endl; std::cin.get(); return 0; }
And the client:
#include <iostream> #include <RCF/RCF.hpp> RCF_BEGIN(I_HelloWorld, "I_HelloWorld") RCF_METHOD_V1(void, Print, const std::string &) RCF_END(I_HelloWorld) int main() { RCF::RcfInitDeinit rcfInit; std::cout << "Calling the I_HelloWorld Print() method." << std::endl; RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); client.Print("Hello World"); return 0; }
The transport used is a TCP connection, with the server listening on port
50001 of the localhost (127.0.0.1
) interface.
Before we can run this code, we need to build it. To build RCF, you simply
need to compile the src/RCF/RCF.cpp
source
file into your application. RCF requires the Boost
library to be available, so you will need to download Boost. Any version
from 1.35.0 onwards will do. You do not need to build any Boost libraries,
as RCF will in its default configuration only use header files from Boost.
Here is how you would build the server code above, with two common compiler toolsets.
New
Project ->
Visual C++ -> Win32 ->
Win32 Console
Application
Application Settings
dialog, check the Empty Project
checkbox.
Project Properties
dialog for the new project, select C/C++ -> General ->
Additional Include
Directories
.
C:\boost_1_49_0
and C:\RCF\include
.
Server.cpp
to the project, and copy-paste
the code above into it.
RCF\src\RCF\RCF.cpp
to the project.
Build ->
Build Solution
.
Server.cpp
and copy-paste the code above into it.
g++ Server.cpp /path/to/RCF/src/RCF/RCF.cpp -I/path/to/boost_1_49_0 -I/path/to/RCF/include -lpthread -ldl -oServer
, replacing /path/to/boost_1_49_0
and /path/to/RCF
with the actual paths on your system.
Let's start the server:
c:\Projects\RcfSample\Debug>Server.exe Press Enter to exit...
, and then the client:
c:\Projects\RcfSample\Debug>Client.exe Calling the I_HelloWorld Print() method. c:\Projects\RcfSample\Debug>
In the server window, you should now see:
c:\Projects\RcfSample\Debug>Server.exe Press Enter to exit... I_HelloWorld service: Hello World
To simplify the rest of the tutorial, we'll rewrite our Hello World example so that the client and server run in a single process:
#include <iostream> #include <RCF/RCF.hpp> RCF_BEGIN(I_HelloWorld, "I_HelloWorld") RCF_METHOD_V1(void, Print, const std::string &) RCF_END(I_HelloWorld) class HelloWorldImpl { public: void Print(const std::string & s) { std::cout << "I_HelloWorld service: " << s << std::endl; } }; int main() { RCF::RcfInitDeinit rcfInit; HelloWorldImpl helloWorld; RCF::RcfServer server( RCF::TcpEndpoint(50001) ); server.bind<I_HelloWorld>(helloWorld); server.start(); std::cout << "Calling the I_HelloWorld Print() method." << std::endl; RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); client.Print("Hello World"); return 0; }
Running this program yields the output:
Calling the I_HelloWorld Print() method. I_HelloWorld service: Hello World
The rest of the tutorial will build on this example, to illustrate some of the fundamental features of RCF.
RCF allows you to define remote interfaces, directly in code. In the example
above, we defined the I_HelloWorld
interface:
RCF_BEGIN(I_HelloWorld, "I_HelloWorld") RCF_METHOD_V1(void, Print, const std::string &) RCF_END(I_HelloWorld)
RCF_BEGIN()
,
RCF_METHOD_xx()
,
and RCF_END()
are macros that are used to generate client and server stubs for remote calls.
RCF_BEGIN()
starts the interface definition, and defines the compile time identifier
of the interface (I_HelloWorld
),
and the runtime identifier of the interface ("I_HelloWorld"
).
RCF_METHOD_xx()
- The RCF_METHOD_xx()
macros define remote methods. RCF_METHOD_V1()
defines a remote method taking one
parameter (in this case const
std::string &
),
and returning void
. RCF_METHOD_R2()
defines a remote method with two parameters and a non-void
return value, and so on. The RCF_METHOD_xx()
macros are defined for void
and non-void
remote calls taking up to 15 parameters.
RCF_END()
ends the interface definition.
On the server, we bind the interface to a servant object:
server.bind<I_HelloWorld>(helloWorld);
Let's add a few more remote methods to the I_HelloWorld
interface. We'll add methods to print a list of strings, and return the number
of characters printed:
// Serialization code for std::vector<>. #include <SF/vector.hpp> RCF_BEGIN(I_HelloWorld, "I_HelloWorld") RCF_METHOD_V1(void, Print, const std::string &) RCF_METHOD_R1(int, Print, const std::vector<std::string> &) RCF_METHOD_V2(void, Print, const std::vector<std::string> &, int &) RCF_END(I_HelloWorld)
Remote calls can return arguments either through a return value (as the second
Print()
method does), or through a non-const reference parameter (as the third Print()
method does).
Having added these methods to the I_HelloWorld
interface, we also need to implement them in the HelloWorldImpl
servant object:
class HelloWorldImpl { public: void Print(const std::string & s) { std::cout << "I_HelloWorld service: " << s << std::endl; } int Print(const std::vector<std::string> & v) { int howManyChars = 0; for (std::size_t i=0; i<v.size(); ++i) { std::cout << "I_HelloWorld service: " << v[i] << std::endl; howManyChars += v[i].size(); } return howManyChars; } void Print(const std::vector<std::string> & v, int & howManyChars) { howManyChars = 0; for (std::size_t i=0; i<v.size(); ++i) { std::cout << "I_HelloWorld service: " << v[i] << std::endl; howManyChars += v[i].size(); } } };
Notice that HelloWorldImpl
is not a derived class. Instead of virtual functions, RCF uses template-based
static polymorphism to bind interfaces to implementations.
Here is sample client code to call the new Print()
methods:
RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); std::vector<std::string> stringsToPrint; stringsToPrint.push_back("AAA"); stringsToPrint.push_back("BBB"); stringsToPrint.push_back("CCC"); // Remote call returning argument through return value. int howManyChars = client.Print(stringsToPrint); // Remote call returning argument through non-const reference parameter. client.Print(stringsToPrint, howManyChars);
And the output:
I_HelloWorld service: AAA I_HelloWorld service: BBB I_HelloWorld service: CCC I_HelloWorld service: AAA I_HelloWorld service: BBB I_HelloWorld service: CCC
If a remote call does not complete, RCF will throw an exception containing
an error message describing the error condition. Let's put a try/catch
wrapper around the remote call:
try { RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); client.Print("Hello World"); } catch(const RCF::Exception & e) { std::cout << "Error: " << e.getErrorString() << std::endl; }
We can simulate the server being down, by commenting out the RcfServer::start()
call:
HelloWorldImpl helloWorld; RCF::RcfServer server( RCF::TcpEndpoint(50001) ); server.bind<I_HelloWorld>(helloWorld); //server.start();
Running the client, we now get this:
Error: Client connection to 127.0.0.1:50001 timed out after 2000 ms (server not started?).
All exceptions thrown by RCF are derived from RCF::Exception
.
RCF::Exception
holds various descriptive and
contextual information about the error. You can call RCF::Exception::getErrorId()
to retrieve the error code, and RCF::Exception::getErrorString()
, to retrieve an English translation of
the error, including any error-specific arguments.
If the server implementation of a remote call throws an exception, it will
be caught by the RcfServer
and returned to the client, where it is thrown as a RCF::RemoteException
.
For example, if we start the server with the following servant implementation:
class HelloWorldImpl { public: void Print(const std::string & s) { throw std::runtime_error("Print() service is unavailable at this time."); std::cout << "I_HelloWorld service: " << s << std::endl; } };
, the client will now output:
Error: Server-side user exception. Exception type: class std::runtime_error. Exception message: "Print() service is unavailable at this time.".
Remote calls are always made through a client stub (RCF::ClientStub
).
You can access the client stub of a RcfClient<>
by calling RcfClient<>::getClientStub()
. The client stub contains a number of settings
that affect how a remote call is executed.
Two of the most important settings are the connection timeout and the remote call timeout. The connection timeout determines how long RCF will wait while trying to establish a network connection to the server. The remote call timeout determines how long RCF will wait for a remote call response to return from the server.
To change these settings, call the ClientStub::setConnectionTimeoutMs()
and ClientStub::setRemoteCallTimeoutMs()
functions:
RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); // 5 second timeout when establishing network connection. client.getClientStub().setConnectTimeoutMs(5*1000); // 60 second timeout when waiting for remote call response from the server. client.getClientStub().setRemoteCallTimeoutMs(60*1000); client.Print("Hello World");
See Remote calls - Client-side for more about client stubs.
On the server-side, RCF maintains a session (RCF::RcfSession
)
for every client connection to the server. The RcfSession
of a client connection is available to server-side code through RCF::getCurrentSession()
. You can use RcfSession
to maintain application data specific to a particular client connection.
Arbitrary C++ objects can be stored in a session by calling RcfSession::createSessionObject<>()
or RcfSession::getSessionObject<>()
.
For instance, to associate a HelloWorldSession
object with each client connection to the I_HelloWorld
interface, which tracks the numer of calls made on a connection:
class HelloWorldSession { public: HelloWorldSession() : mCallCount(0) { std::cout << "Created HelloWorldSession object." << std::endl; } ~HelloWorldSession() { std::cout << "Destroyed HelloWorldSession object." << std::endl; } std::size_t mCallCount; }; class HelloWorldImpl { public: void Print(const std::string & s) { RCF::RcfSession & session = RCF::getCurrentRcfSession(); // Creates the session object if it doesn't already exist. HelloWorldSession & hwSession = session.getSessionObject<HelloWorldSession>(true); ++hwSession.mCallCount; std::cout << "I_HelloWorld service: " << s << std::endl; std::cout << "I_HelloWorld service: " << "Total calls on this connection so far: " << hwSession.mCallCount << std::endl; } };
Distinct RcfClient<>
instances will have distinct connections to the server. Here we are calling
Print()
three times from two RcfClient<>
instances:
for (std::size_t i=0; i<2; ++i) { RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); client.Print("Hello World"); client.Print("Hello World"); client.Print("Hello World"); } // Wait a little so the server has time to destroy the last session. RCF::sleepMs(1000);
, resulting in the following output:
Created HelloWorldSession object. I_HelloWorld service: Hello World I_HelloWorld service: Total calls on this connection so far: 1 I_HelloWorld service: Hello World I_HelloWorld service: Total calls on this connection so far: 2 I_HelloWorld service: Hello World I_HelloWorld service: Total calls on this connection so far: 3 Destroyed HelloWorldSession object. Created HelloWorldSession object. I_HelloWorld service: Hello World I_HelloWorld service: Total calls on this connection so far: 1 I_HelloWorld service: Hello World I_HelloWorld service: Total calls on this connection so far: 2 I_HelloWorld service: Hello World I_HelloWorld service: Total calls on this connection so far: 3 Destroyed HelloWorldSession object.
The server session and any associated session objects are destroyed when the client connection is closed.
See Remote calls - Server-side for more about server sessions.
RCF makes it easy to change the underlying transport of a remote call. The
transport layer is determined by the endpoint parameters passed to RcfServer
and RcfClient<>
. So far we've been using TcpEndpoint
, to specify a TCP transport.
By default, when you specify a TcpEndpoint
with only a port number, RCF will use 127.0.0.1
as the
IP address. So the following two snippets are equivalent:
RCF::RcfServer server( RCF::TcpEndpoint(50001) );
RCF::RcfServer server( RCF::TcpEndpoint("127.0.0.1", 50001) );
, as are the following:
RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) );
RcfClient<I_HelloWorld> client( RCF::TcpEndpoint("127.0.0.1", 50001) );
127.0.0.1
is the IPv4 loopback address. A server
listening on 127.0.0.1
will only be available to clients on the
same machine as the server. It's likely you'll want to run clients across
the network, in which case the server will need to listen on an externally
visible network address. The easiest way to do that is to specify 0.0.0.0
(for IPv4), or ::0
(for IPv6) instead, which will make the server listen on all available network
interfaces:
// Server-side. RCF::RcfServer server( RCF::TcpEndpoint("0.0.0.0", 50001) ); // Client-side. RcfClient<I_HelloWorld> client( RCF::TcpEndpoint("Server123", 50001) );
RCF supports a number of other endpoint types as well. To run the server
and client over UDP, use UdpEndpoint
:
// Server-side. RCF::RcfServer server( RCF::UdpEndpoint("0.0.0.0", 50001) ); // Client-side. RcfClient<I_HelloWorld> client( RCF::UdpEndpoint("Server123", 50001) );
To run the server and client over named pipes, use NamedPipeEndpoint
:
// Server-side. RCF::RcfServer server( RCF::Win32NamedPipeEndpoint("MyPipe") ); // Client-side. RcfClient<I_HelloWorld> client( RCF::Win32NamedPipeEndpoint("MyPipe") );
NamedPipeEndpoint
maps onto
Windows
named pipes on Windows systems, and UNIX
local domain sockets on UNIX-based systems.
It's usually not practical to use UDP for two-way (request/response) messaging, as the unreliable semantics of UDP mean that messages may not be delivered, or may be delivered out of order. UDP is useful in one-way messaging scenarios, where the server application logic is resilient to messages being lost or arriving out of order.
RCF supports tunneling of remote calls over the HTTP and HTTPS protocols.
RCF::HttpEndpoint
is used to configure tunneling
over HTTP. Here is an example of a client making remote calls to a server,
through a third party HTTP proxy:
// Server-side. HelloWorldImpl helloWorldImpl; RCF::RcfServer server( RCF::HttpEndpoint("0.0.0.0", 80) ); server.bind<I_HelloWorld>(helloWorldImpl); server.start(); // Client-side. // This client will connect to server1.acme.com via the HTTP proxy at proxy.acme.com:8080. RcfClient<I_HelloWorld> client( RCF::HttpEndpoint("server1.acme.com", 80) ); client.getClientStub().setHttpProxy("proxy.acme.com"); client.getClientStub().setHttpProxyPort(8080); client.Print("Hello World");
Similarly, RCF::HttpsEndpoint
can be used to configure
tunneling over HTTPS:
// Server-side. HelloWorldImpl helloWorldImpl; RCF::RcfServer server( RCF::HttpsEndpoint("0.0.0.0", 443) ); server.bind<I_HelloWorld>(helloWorldImpl); RCF::CertificatePtr serverCertPtr( new RCF::PfxCertificate("path/to/certificate.p12", "password", "CertificateName") ); server.setCertificate(serverCertPtr); server.start(); // Client-side. // This client will connect to server1.acme.com via the HTTP proxy at proxy.acme.com:8080. RcfClient<I_HelloWorld> client( RCF::HttpsEndpoint("server1.acme.com", 443) ); client.getClientStub().setHttpProxy("proxy.acme.com"); client.getClientStub().setHttpProxyPort(8080); client.Print("Hello World");
Certificates and certificate validation for HTTPS are configured as for the SSL transport protocol (see Transport Protocols below).
Multiple transports can be configured for a single RcfServer
.
For example, to configure a server that accepts both IPv4 and IPv6 connections
on port 50001:
RCF::RcfServer server; server.addEndpoint( RCF::TcpEndpoint("0.0.0.0", 50001) ); server.addEndpoint( RCF::TcpEndpoint("::0", 50001) ); server.start();
On some platforms, the underlying network stack will allow you to specify
::0
to listen on both IPv4 and IPv6 interfaces, in which case this would be sufficient:
RCF::RcfServer server; server.addEndpoint( RCF::TcpEndpoint("::0", 50001) ); server.start();
Here is a server accepting connections over TCP, UDP and named pipes:
RCF::RcfServer server; server.addEndpoint( RCF::TcpEndpoint("::0", 50001) ); server.addEndpoint( RCF::UdpEndpoint("::0", 50002) ); server.addEndpoint( RCF::Win32NamedPipeEndpoint("MyPipe") ); server.start();
See Transports for more information on transports.
Transport protocols are layered on top of the transport, and provide authentication and encryption.
RCF supports the following transport protocols:
The NTLM and Kerberos transport protocols are only supported on Windows, while the SSL transport protocol is supported on all platforms.
An RcfServer
can be configured
to require certain transport protocols:
std::vector<RCF::TransportProtocol> protocols; protocols.push_back(RCF::Tp_Ntlm); protocols.push_back(RCF::Tp_Kerberos); server.setSupportedTransportProtocols(protocols);
On the client-side, ClientStub::setTransportProtocol()
is used to configure transport protocols.
For instance, to use the NTLM protocol on a client connection:
RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); client.getClientStub().setTransportProtocol(RCF::Tp_Ntlm); client.Print("Hello World");
In this example, the client will authenticate itself to the server and encrypt its connection using the NTLM protocol, using the implicit credentials of the logged on user.
To provide explicit credentials, use ClientStub::setUsername()
and ClientStub::setPassword()
:
RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); client.getClientStub().setTransportProtocol(RCF::Tp_Ntlm); client.getClientStub().setUsername("SomeDomain\\Joe"); client.getClientStub().setPassword("JoesPassword"); client.Print("Hello World");
The Kerberos protocol can be configured similarly. The Kerberos protocol
requires the client to supply the Service Principal Name (SPN) of the server.
You can do this by calling ClientStub::setKerberosSpn()
:
RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); client.getClientStub().setTransportProtocol(RCF::Tp_Kerberos); client.getClientStub().setKerberosSpn("SomeDomain\\ServerAccount"); client.Print("Hello World");
If a client attempts to call Print()
without configuring one of the transport
protocols required by the server, they will get an error:
try { RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); client.Print("Hello World"); } catch(const RCF::Exception & e) { std::cout << "Error: " << e.getErrorString() << std::endl; }
Error: Server requires one of the following transport protocols to be used: NTLM, Kerberos.
From within the server implementation of Print()
, you can retrieve the transport protocol
of the current session. If the transport protocol is NTLM or Kerberos, you
can also retrieve the username of the client, as well as impersonate the
client:
class HelloWorldImpl { public: void Print(const std::string & s) { RCF::RcfSession & session = RCF::getCurrentRcfSession(); RCF::TransportProtocol protocol = session.getTransportProtocol(); if ( protocol == RCF::Tp_Ntlm || protocol == RCF::Tp_Kerberos ) { std::string clientUsername = session.getClientUsername(); RCF::SspiImpersonator impersonator(session); // Now running under Windows credentials of client. // ... // Impersonation ends when we exit scope. } std::cout << s << std::endl; } };
RCF also supports using SSL as the transport protocol. RCF provides two SSL
implementations, one based on OpenSSL, and the other based on the Windows
Schannel security package. The Windows Schannel implementation is used automatically
on Windows, while the OpenSSL implementation is used if RCF_USE_OPENSSL
is defined when building RCF (see Building).
SSL-enabled servers need to configure a SSL certificate. The mechanics of certificate configuration vary depending on which implementation is being used. Here is an example using the Schannel SSL implementation:
RCF::RcfServer server( RCF::TcpEndpoint(50001) ); RCF::CertificatePtr serverCertificatePtr( new RCF::PfxCertificate( "C:\\ServerCert.p12", "Password", "CertificateName") ); server.setCertificate(serverCertificatePtr); server.start(); RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); client.getClientStub().setTransportProtocol(RCF::Tp_Ssl); client.getClientStub().setEnableSchannelCertificateValidation("localhost");
RCF::PfxCertificate
is used to load PKCS #12
certificates, from .pfx and .p12 files. RCF also provides the RCF::StoreCertificate
class, to load certificates from a Windows certificate store.
When using the OpenSSL-based SSL implementation, the RCF::PemCertificate
class is used to load PEM certificates, from .pem files.
RCF also supports link level compression of remote calls. Compression is
configured independently of transport protocols, using ClientStub::setEnableCompression()
. The compression stage is applied immediately
before the transport protocol stage.
Here is an example of a client connecting to a server through an HTTP proxy, using NTLM for authentication and encryption, and with compression enabled:
RcfClient<I_HelloWorld> client( RCF::HttpEndpoint("server123.com", 80) ); client.getClientStub().setHttpProxy("proxy.mycompany.com"); client.getClientStub().setHttpProxyPort(8080); client.getClientStub().setTransportProtocol(RCF::Tp_Ntlm); client.getClientStub().setEnableCompression(true); client.Print("Hello World");
For more information see Transport protocols.
By default a RcfServer
uses
a single thread to handle incoming remote calls, so remote calls are dispatched
serially, one after the other.
This can be a problem if you have multiple clients and some of them are making
calls that take significant time to complete. To improve responsiveness in
these situations, a RcfServer
can be configured to run multiple threads and dispatch remote calls in parallel.
To configure a multi-threaded RcfServer
,
use RcfServer::setThreadPool()
to assign a thread pool to the server. RCF thread pools can be configured
to use either a fixed number of threads:
RCF::RcfServer server( RCF::TcpEndpoint(50001) ); // Thread pool with fixed number of threads (5). RCF::ThreadPoolPtr tpPtr( new RCF::ThreadPool(5) ); server.setThreadPool(tpPtr); server.start();
, or a dynamic number of threads, varying with server load:
RCF::RcfServer server( RCF::TcpEndpoint(50001) ); // Thread pool with varying number of threads (1 to 25). RCF::ThreadPoolPtr tpPtr( new RCF::ThreadPool(1, 25) ); server.setThreadPool(tpPtr); server.start();
Thread pools configured through RcfServer::setThreadPool()
are shared across all the transports of
that RcfServer
. It is also
possible to use I_ServerTransport::setThreadPool()
to configure thread pools for individual
transports:
RCF::RcfServer server; RCF::ServerTransport & tcpTransport = server.addEndpoint(RCF::TcpEndpoint(50001)); RCF::ServerTransport & pipeTransport = server.addEndpoint(RCF::Win32NamedPipeEndpoint("MyPipe")); // Thread pool with up to 5 threads to serve TCP clients. RCF::ThreadPoolPtr tcpThreadPoolPtr( new RCF::ThreadPool(1, 5) ); tcpTransport.setThreadPool(tcpThreadPoolPtr); // Thread pool with single thread to serve named pipe clients. RCF::ThreadPoolPtr pipeThreadPoolPtr( new RCF::ThreadPool(1) ); pipeTransport.setThreadPool(pipeThreadPoolPtr); server.start();
RCF supports client-side asynchronous remote call invocation, and server-side asynchronous remote call dispatching.
On the client-side, asynchronous remote calls allow you to avoid blocking the thread that is making a remote call. Asynchronous remote calls complete on background threads, and the calling thread is notified at a later point, when the call completes.
Asynchronous remote call invocation is implemented in RCF using the RCF::Future<>
template. Here is a simple example of waiting for an asynchronous call
to complete:
HelloWorldImpl helloWorld; RCF::RcfServer server( RCF::TcpEndpoint(50001) ); server.bind<I_HelloWorld>(helloWorld); server.start(); RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); RCF::Future<int> fRet; // Asynchronous remote call. fRet = client.Print("Hello World"); // Wait for the call to complete. while (!fRet.ready()) RCF::sleepMs(1000); // Check for errors. std::auto_ptr<RCF::Exception> ePtr = client.getClientStub().getAsyncException(); if (ePtr.get()) { // Error handling. // ... } else { int howManyCharsPrinted = *fRet; }
A remote call is performed asynchronously, if any of the parameters, or
the return value, is of type RCF::Future<>
, or if RCF::AsyncOneway
or RCF::AsyncTwoway
is specified as the calling
semantic. Once a call is complete, RCF::Future<>
instances can be dereferenced to
retrieve the return values. If the call completed with an error, the error
can be retrieved by calling RCF::ClientStub::getAsyncException()
. The error will also be thrown if an
attempt is made to dereference an associated RCF::Future<>
instance.
Instead of waiting for the result on the calling thread, we can assign a completion callback to the remote call:
typedef boost::shared_ptr< RcfClient<I_HelloWorld> > HelloWorldPtr; void onPrintCompleted( RCF::Future<int> fRet, HelloWorldPtr clientPtr) { int howManyCharsPrinted = *fRet; }
HelloWorldPtr clientPtr( new RcfClient<I_HelloWorld>(RCF::TcpEndpoint(50001)) ); RCF::Future<int> fRet; // Asynchronous remote call, with completion callback. fRet = clientPtr->Print( RCF::AsyncTwoway( boost::bind(&onPrintCompleted, fRet, clientPtr) ), "Hello World");
We've passed both the Future<int>
return value and a reference counted
client (HelloWorldPtr
)
to the callback. If we were to destroy the client on the main thread, the
asynchronous call would be automatically canceled, and the callback would
not be called at all.
Asynchronous remote calls are useful in many circumstances. For instance,
the following code will call Print()
once every 10 seconds on 50 different
servers:
void onPrintCompleted(HelloWorldPtr clientPtr); void onWaitCompleted(HelloWorldPtr clientPtr); void onPrintCompleted(HelloWorldPtr clientPtr) { // Print() call completed. Wait for 10 seconds. clientPtr->getClientStub().wait( boost::bind(&onWaitCompleted, clientPtr), 10*1000); } void onWaitCompleted(HelloWorldPtr clientPtr) { // 10 second wait completed. Make another Print() call. clientPtr->Print( RCF::AsyncTwoway( boost::bind(onPrintCompleted, clientPtr)), "Hello World"); }
// Addresses to 50 servers. std::vector<RCF::TcpEndpoint> servers(50); // ... // Create a connection to each server, and make the first call. std::vector<HelloWorldPtr> clients; for (std::size_t i=0; i<50; ++i) { HelloWorldPtr clientPtr( new RcfClient<I_HelloWorld>(servers[i]) ); clients.push_back(clientPtr); // Asynchronous remote call, with completion callback. clientPtr->Print( RCF::AsyncTwoway( boost::bind(&onPrintCompleted, clientPtr)), "Hello World"); } // All 50 servers are now being called once every 10 s. // ... // Upon leaving scope, the clients are all automatically destroyed. Any // remote calls in progress are automatically canceled.
Instead of blocking 50 threads with synchronous remote calls, all 50 connections are managed by a single thread. The remote call sequences to the 50 servers are all handled on a background RCF thread, and the connections are automatically destroyed when the main thread leaves scope.
RCF also supports server-side asynchronous remote call dispatching. When
a remote call arrives at a RcfServer
,
it is handled by one of the threads in the servers thread pool. To process
the call asynchronously on another thread, you can use the RCF::RemoteCallContext<>
template to capture the server-side context of the remote call, and queue
it for later processing.
Here is an example of using RemoteCallContext<>
in server-side code. Instead of
responding to Print()
calls in HelloWorldImpl::Print()
, we are passing the remote call contexts
to a background thread to be processed in bulk once every second. The background
thread uses RemoteCallContext::parameters()
to access the parameters of each remote
call, and RemoteCallContext::commit()
to send the remote call response back
to the client.
#include <RCF/RemoteCallContext.hpp> RCF_BEGIN(I_HelloWorld, "I_HelloWorld") RCF_METHOD_R1(int, Print, const std::string &) RCF_END(I_HelloWorld) // I_HelloWorld servant object class HelloWorldImpl { public: typedef RCF::RemoteCallContext<int, const std::string &> PrintCall; int Print(const std::string & s) { // Capture the remote call context and queue it in mPrintCalls. RCF::Lock lock(mPrintCallsMutex); mPrintCalls.push_back( PrintCall(RCF::getCurrentRcfSession()) ); // Dummy return value. return 0; } HelloWorldImpl() { // Start the asynchronous printing thread. mStopFlag = false; mPrintThreadPtr.reset( new RCF::Thread( boost::bind( &HelloWorldImpl::processPrintCalls, this) ) ); } ~HelloWorldImpl() { // Stop the asynchronous printing thread. mStopFlag = true; mPrintThreadPtr->join(); } private: // Queue of remote calls. RCF::Mutex mPrintCallsMutex; std::deque<PrintCall> mPrintCalls; // Asynchronous printing thread. RCF::ThreadPtr mPrintThreadPtr; volatile bool mStopFlag; void processPrintCalls() { // Once a second, process all queued Print() calls. while (!mStopFlag) { Sleep(1000); // Retrieve all queued print calls. std::deque<PrintCall> printCalls; { RCF::Lock lock(mPrintCallsMutex); printCalls.swap(mPrintCalls); } // Process them. for (std::size_t i=0; i<printCalls.size(); ++i) { PrintCall & printCall = printCalls[i]; const std::string & stringToPrint = printCall.parameters().a1.get(); std::cout << "I_HelloWorld service: " << stringToPrint << std::endl; printCall.parameters().r.set( stringToPrint.size() ); printCall.commit(); } } } };
The client code is unchanged:
int main() { RCF::RcfInitDeinit rcfInit; HelloWorldImpl helloWorld; RCF::RcfServer server( RCF::TcpEndpoint(50001) ); server.bind<I_HelloWorld>(helloWorld); server.start(); RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); int charsPrinted = client.Print("Hello World"); return 0; }
For more information, see Asynchronous Remote Calls.
RCF makes it easy to setup publish/subscribe feeds. For example, here is
a publisher publishing "Hello World"
once every second:
RCF::RcfServer publishingServer( RCF::TcpEndpoint(50001) ); publishingServer.start(); // Start publishing. typedef boost::shared_ptr< RCF::Publisher<I_HelloWorld> > HelloWorldPublisherPtr; HelloWorldPublisherPtr pubPtr = publishingServer.createPublisher<I_HelloWorld>(); while (shouldContinue()) { Sleep(1000); // Publish a Print() call to all currently connected subscribers. pubPtr->publish().Print("Hello World"); } // Close the publisher. pubPtr->close();
To create a subscription to the publisher:
// Start a subscriber. RCF::RcfServer subscriptionServer(( RCF::TcpEndpoint() )); subscriptionServer.start(); HelloWorldImpl helloWorld; RCF::SubscriptionPtr subPtr = subscriptionServer.createSubscription<I_HelloWorld>( helloWorld, RCF::TcpEndpoint(50001)); // At this point Print() will be called on the helloWorld object once a second. // ... // Close the subscription. subPtr->close();
Each call the publisher makes is sent as a oneway call to all subscribers.
For more information on publish/subscribe messaging, see Publish/subscribe.
So far we've seen clients making remote calls to servers. It's also possible
for a server to make remote calls back to a client, once the client has established
a connection. To do this, the client starts a RcfServer
of its own, and calls RcfServer::createCallbackConnection()
:
// Client-side int main() { RCF::RcfInitDeinit rcfInit; // Client needs a RcfServer to accept callback connections. RCF::RcfServer callbackServer(( RCF::TcpEndpoint() )); HelloWorldImpl helloWorld; callbackServer.bind<I_HelloWorld>(helloWorld); callbackServer.start(); // Establish client connection to server. RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); // Create the callback connection. RCF::createCallbackConnection(client, callbackServer); // Server can now call Print() on the helloWorld object. // ... return 0; }
On the server-side, RcfServer::setCallbackConnectionCb()
is used to take control over the callback
connection once it has been created by the client:
// Server-side typedef boost::shared_ptr< RcfClient<I_HelloWorld> > HelloWorldPtr; RCF::Mutex gCallbackClientsMutex; std::vector< HelloWorldPtr > gCallbackClients; void onCallbackConnectionCreated( RCF::RcfSessionPtr sessionPtr, RCF::ClientTransportAutoPtr transportAutoPtr) { typedef boost::shared_ptr< RcfClient<I_HelloWorld> > HelloWorldPtr; HelloWorldPtr helloWorldPtr( new RcfClient<I_HelloWorld>(transportAutoPtr) ); RCF::Lock lock(gCallbackClientsMutex); gCallbackClients.push_back( helloWorldPtr ); } int main() { RCF::RcfInitDeinit rcfInit; RCF::RcfServer server( RCF::TcpEndpoint(50001) ); server.setOnCallbackConnectionCreated( boost::bind(&onCallbackConnectionCreated, _1, _2) ); server.start(); // Wait for clients to create callback connections. // ... // Retrieve all created callback connections. std::vector<HelloWorldPtr> clients; { RCF::Lock lock(gCallbackClientsMutex); clients.swap(gCallbackClients); } // Call Print() on them. for (std::size_t i=0; i<clients.size(); ++i) { HelloWorldPtr clientPtr = clients[i]; clientPtr->Print("Hello World"); } return 0; }
Callback connections function just like regular connections, with the exception
that callback connections cannot be reconnected. If a callback connection
is lost, the client will need to connect again and call RCF::createCallbackConnection()
, to re-establish connectivity.
For more information on callback connections, see Callback Connections.
File downloads and uploads are common in distributed systems. RCF provides
built-in support for file transfers, through the RCF::FileDownload
and RCF::FileUpload
classes. RCF::FileDownload
and RCF::FileUpload
are used as remote method arguments,
to implement file transfers as part of a remote call.
On the server-side, file transfer functionality is by default disabled, and needs to be explicitly enabled:
HelloWorldImpl helloWorld; RCF::RcfServer server( RCF::TcpEndpoint(50001) ); server.bind<I_HelloWorld>(helloWorld); server.start();
Here we've implemented a PrintAndDownload()
method, that allows clients to download
files:
RCF_BEGIN(I_HelloWorld, "I_HelloWorld") RCF_METHOD_V1(void, Print, const std::string &) RCF_METHOD_V2(void, PrintAndDownload, const std::string &, RCF::FileDownload) RCF_END(I_HelloWorld) class HelloWorldImpl { public: void Print(const std::string & s) { std::cout << "I_HelloWorld service: " << s << std::endl; } void PrintAndDownload(const std::string & s, RCF::FileDownload fileDownload) { std::cout << "I_HelloWorld service: " << s << std::endl; fileDownload = RCF::FileDownload("path/to/download"); } };
, which can be called like this:
RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); RCF::FileDownload fileDownload("path/to/download/to"); client.PrintAndDownload("Hello World", fileDownload); std::string pathToDownload = fileDownload.getLocalPath(); RCF::FileManifest & downloadManifest = fileDownload.getManifest(); std::cout << "Client-local path to upload: " << pathToDownload << std::endl; std::cout << "Number of files uploaded: " << downloadManifest.mFiles.size() << std::endl;
Here we've implemented a PrintAndUpload()
method, that allows clients to upload files:
RCF_BEGIN(I_HelloWorld, "I_HelloWorld") RCF_METHOD_V1(void, Print, const std::string &) RCF_METHOD_V2(void, PrintAndUpload, const std::string &, RCF::FileUpload) RCF_END(I_HelloWorld) class HelloWorldImpl { public: void Print(const std::string & s) { std::cout << "I_HelloWorld service: " << s << std::endl; } void PrintAndUpload(const std::string & s, RCF::FileUpload fileUpload) { std::cout << "I_HelloWorld service: " << s << std::endl; std::string pathToUpload = fileUpload.getLocalPath(); RCF::FileManifest & uploadManifest = fileUpload.getManifest(); std::cout << "Server-local path to upload: " << pathToUpload << std::endl; std::cout << "Number of files uploaded: " << uploadManifest.mFiles.size() << std::endl; } };
, which can be called like this:
// Upload files to server. RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); RCF::FileUpload fileUpload("path/to/files"); client.PrintAndUpload("Hello World", fileUpload);
File transfers can be monitored, paused, resumed, and canceled , and bandwidth throttles can be applied to file transfers to and from a given server.
For more information, see File transfers.
RCF provides native integration with Protocol Buffers. Classes that are generated by the Protocol Buffers compiler can be used in RCF interfaces, and serialization and deserialization is performed through the relevant Protocol Buffers functions.
For example, running the protoc
compiler over the following Protobuf interface:
// Person.proto message Person { required int32 id = 1; required string name = 2; optional string email = 3; } message PbEmpty { optional string log = 1; }
protoc Person.proto --cpp_out=.
, generates the C++ class Person
,
defined in Person.pb.h
.
By including Person.pb.h
,
we can send Person
instances
to our Print()
function:
#include <../test/protobuf/messages/cpp/Person.pb.h> RCF_BEGIN(I_HelloWorld, "I_HelloWorld") RCF_METHOD_V1(void, Print, const std::string &) RCF_METHOD_V1(void, Print, const Person &) RCF_END(I_HelloWorld) class HelloWorldImpl { public: void Print(const std::string & s) { std::cout << s << std::endl; } void Print(const Person & person) { std::cout << "Person name: " << person.name(); std::cout << "Person email: " << person.email(); std::cout << "Person id: " << person.id(); } }; int main() { RCF::RcfInitDeinit rcfInit; HelloWorldImpl helloWorld; RCF::RcfServer server( RCF::TcpEndpoint(50001) ); server.bind<I_HelloWorld>(helloWorld); server.start(); RcfClient<I_HelloWorld> client( RCF::TcpEndpoint(50001) ); Person person; person.set_name("Bob"); person.set_email("bob@acme.com"); person.set_id(123); client.Print(person); return 0; }
For more information on Protocol Buffers support, see Protocol Buffers.
RCF provides a built in JSON-RPC server, allowing access to C++ server functionality from JSON-RPC clients, such as Javascript code on web pages. RCF supports JSON-RPC over both HTTP and HTTPS.
RCF uses the JSON
Spirit library to read and write JSON messages. You will need to
download this library separately, and define RCF_USE_JSON
when building RCF, to enable JSON-RPC support (see Appendix
- Building).
To configure a JSON-RPC endpoint, call I_ServerTransport::setRpcProtocol()
on the relevant server transport. To expose
servant objects to JSON-RPC clients, use RcfServer::bindJsonRpc()
.
Here is an example of a RCF server using multiple transports to accept both RCF requests and JSON-RPC requests:
RCF_BEGIN(I_HelloWorld, "I_HelloWorld") RCF_METHOD_V1(void, Print, const std::string &) RCF_END(I_HelloWorld) class HelloWorldImpl { public: void Print(const std::string & s) { std::cout << "I_HelloWorld service: " << s << std::endl; } void JsonPrint( const RCF::JsonRpcRequest & request, RCF::JsonRpcResponse & response) { // Print out all the strings passed in, and return the number of // characters printed. int charsPrinted = 0; const json_spirit::Array & params = request.getJsonParams(); for (std::size_t i=0; i<params.size(); ++i) { const std::string & s = params[i].get_str(); std::cout << "I_HelloWorld service: " << s << std::endl; charsPrinted += s.size(); } // Return number of characters printed. json_spirit::mObject & responseObj = response.getJsonResponse(); responseObj["result"] = charsPrinted; } }; int main() { RCF::RcfInitDeinit rcfInit; RCF::RcfServer server; // Accept RCF client requests on port 50001. HelloWorldImpl helloWorld; server.bind<I_HelloWorld>(helloWorld); server.addEndpoint( RCF::TcpEndpoint(50001) ); // Accept JSON-RPC requests over HTTP on port 80. server.bindJsonRpc( boost::bind(&HelloWorldImpl::JsonPrint, &helloWorld, _1, _2), "JsonPrint"); server.addEndpoint( RCF::HttpEndpoint(80) ) .setRpcProtocol(RCF::Rp_JsonRpc); server.start(); // RCF clients can call Print() on port 50001. // ... // JSON-RPC clients can call JsonPrint() over HTTP on port 80. // ... return 0; }
For more information on JSON-RPC servers, see JSON-RPC.