PrevUpHomeNext

Tutorial

Getting started
Interfaces and implementations
Error handling
Client stubs
Server sessions
Transports
Transport protocols
Server-side threading
Asynchronous remote calls
Publish/subscribe
Callback connections
File transfers
Protocol Buffers
JSON-RPC

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.

  • In the Visual Studio IDE, select New Project -> Visual C++ -> Win32 -> Win32 Console Application
  • In the Application Settings dialog, check the Empty Project checkbox.
  • In the Project Properties dialog for the new project, select C/C++ -> General -> Additional Include Directories.
  • Add the include paths for Boost and RCF, e.g. C:\boost_1_49_0 and C:\RCF\include.
  • Add a file Server.cpp to the project, and copy-paste the code above into it.
  • Add RCF\src\RCF\RCF.cpp to the project.
  • Select Build -> Build Solution.
  • Create a file Server.cpp and copy-paste the code above into it.
  • From the same directory, run the following command:

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.


PrevUpHomeNext