Remote Call Framework 3.4
Server-side Programming

On the server-side, RCF::RcfServer is the fundamental class responsible for dispatching remote calls from clients. A RcfServer contains one or more transports, on which it listens for remote calls from clients. A RcfServer also exposes one or more servant bindings, to which it dispatches remote calls that come in from clients.

A minimal example of a server was shown in the Tutorial. First, a RcfServer is created with a TCP transport:

RCF::RcfServer server( RCF::TcpEndpoint(50001) );

Then a servant binding is configured for the I_PrintService interface:

PrintService printService;
server.bind<I_PrintService>(printService);

Finally, the server is started and begins to respond to remote calls from clients:

server.start();

Configuring a Server

Adding Transports

A RcfServer listens on one or more transports, for remote calls from clients. If you are setting up a server with only a single transport, you can supply the corresponding RCF::Endpoint parameter to the RcfServer constructor:

RCF::RcfServer server( RCF::TcpEndpoint(50001) );

Alternatively, to configure multiple transports, you can use RCF::RcfServer::addEndpoint():

server.addEndpoint( RCF::TcpEndpoint(50001) );
server.addEndpoint( RCF::UdpEndpoint(50002) );

To perform additional transport-related configuration, you can capture the return value from RCF::RcfServer::addEndpoint():

RCF::ServerTransport& tcpTransport = server.addEndpoint(RCF::TcpEndpoint(50001));
tcpTransport.setConnectionLimit(100);
tcpTransport.setMaxIncomingMessageLength(1024 * 1024);

Adding Servant Bindings

Servant objects are responsible for the actual server functionality of your application. When a RcfServer receives a remote call request from a client, it uses the servant binding name specified in the request to locate the relevant servant binding, and then dispatches the remote call to the relevant function on that servant object.

Servant objects are always bound to a RCF interface. To create a servant binding, use RCF::RcfServer::bind<>(). Each binding on the server is identified by its binding name, which is normally the runtime name of the RCF interface used in the binding.

So the following code:

server.bind<I_PrintService>(printService);

, creates a servant binding with the servant binding name "I_PrintService".

The servant binding name can also be set explicitly:

server.bind<I_PrintService>(printService, "CustomBindingName");

Every remote call request from a client includes a servant binding name, which is used by the server to dispatch the remote call. The default servant binding name supplied by a RCF client is the runtime name of the RCF interface it is using. So the following client code:

RcfClient<I_PrintService> client( RCF::TcpEndpoint(50001) );
client.Print("Hello World");

, makes a remote call which is dispatched by the server to a servant object with the servant binding name "I_PrintService".

The client can also set the servant binding name explicitly:

RcfClient<I_PrintService> client( RCF::TcpEndpoint(50001), "CustomBindingName" );
client.Print("Hello World");

, to dispatch the call to the nominated servant binding.

Starting and Stopping a Server

A RcfServer instance will not start dispatching remote calls until RCF::RcfServer::start() is called.

The server can be stopped manually by calling RCF::RcfServer::stop():

RCF::RcfServer server( RCF::TcpEndpoint(50001) );
server.start();
// ...
server.stop();

The server will be stopped automatically if the RcfServer object goes out of scope.

Server Threading

By default, a RcfServer will use a single thread to dispatch calls across all its transports. This behavior can be modified by explicitly assigning a thread pool to the RcfServer:

RCF::RcfServer server( RCF::TcpEndpoint(50001) );
RCF::ThreadPoolPtr threadPoolPtr( new RCF::ThreadPool(1, 5) );
server.setThreadPool(threadPoolPtr);

RCF::ThreadPool can be configured to use a fixed number of threads, or a varying number of threads depending on server load:

// Thread pool with a fixed number of threads (5).
RCF::ThreadPoolPtr threadPoolPtr( new RCF::ThreadPool(5) );
server.setThreadPool(threadPoolPtr);
// Thread pool with a varying number of threads (1-25).
RCF::ThreadPoolPtr threadPoolPtr( new RCF::ThreadPool(1, 25) );
server.setThreadPool(threadPoolPtr);

A thread pool assigned to the RcfServer will be shared by all the transports of that RcfServer.

You can also use RCF::ServerTransport::setThreadPool() to assign thread pools to specific transports:

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

Server-side Sessions

RCF creates a server-side RCF::RcfSession object for each connection to the server. The RcfSession object has a lifetime that matches that of the client connection, and provides a mechanism for server-side code to persist state across remote calls on the same client connection.

From within a servant object executing a remote call, you can access the RcfSession by calling RCF::getCurrentRcfSession():

class PrintService
{
public:
void Print(const std::string & s)
{
std::cout << "I_PrintService service: " << s << std::endl;
// ...
}
};

Session Objects

Session objects are application C++ objects that are stored in the RCF session, and thus persisted across remote calls on the same connection. A typical use case for session objects is to associate application specific information with a connection. For example, application logic may require that after a client connects, the first thing it should do is call a Login() method. If the Login() method succeeds, the authenticated state of the connections needs to be persisted so that subsequent calls on the same connection can access it.

The following functions provide access to session objects:

Session objects can be of arbitrary type, and their lifetime is controlled by the lifetime of the client connection. When the client connection is closed, any session objects associated with it are destroyed.

For an example of using session objects to store authentication state, see Access Control.

Custom Request and Response Data

As described in Client-side Programming, a remote call can have extra user data associated with it, apart from the parameters of the remote call. Both the remote call request and the remote call response can carry such data, and you can use the following RcfSession functions to access that data:

Session Information

You can use RcfSession to find out a number of things about the current client connection, including:

  • The transport being used:
  • The network address of the client:
const RCF::RemoteAddress & clientAddress = session.getClientAddress();
std::string strClientAddress = clientAddress.string();
  • The current request header:
  • How long the client has been connected:
// When the connection was established, as reported by CRT time() function.
time_t connectedAt = session.getConnectedAtTime();
// Connection duration in seconds.
std::size_t connectionDurationS = session.getConnectionDuration();
  • How many calls have been made on this connection:
std::size_t callsMade = session.getRemoteCallCount();
  • How many bytes have been sent and received on this connection:
std::uint64_t totalBytesReceived = session.getTotalBytesReceived();
std::uint64_t totalBytesSent = session.getTotalBytesSent();
  • The clients user name, if the client has been authenticated:
std::string clientUserName = session.getClientUserName();
  • The SSL certificate the client has presented (if any):
RCF::CertificatePtr clientCertPtr = session.getClientCertificatePtr();

For more information, see the reference documentation for RCF::RcfSession.

Access Control

RCF allows you to apply access controls to individual servant bindings on your server. The access control is implemented as a user-defined callback function, in which you can apply application-specific logic to determine whether a client connection should be allowed to access a particular servant binding.

For example:

bool onPrintServiceAccess(int methodId)
{
// Return true to allow access, and false to deny access.
// ...
return true;
}
PrintService printService;
RCF::RcfServer server( RCF::TcpEndpoint(50001) );
RCF::ServerBindingPtr bindingPtr = server.bind<I_PrintService>(printService);
auto accessControl = [&](int methodId) { return onPrintServiceAccess(methodId); };
bindingPtr->setAccessControl(accessControl);
server.start();

The access control callback will be invoked by the RcfServer each time a client tries to call a method on that servant binding.

From the access control callback you can inspect the current session and determine whether it should be granted access to the servant. Once authentication is granted, you will probably want to store the authentication state in a session object, so it can be easily reused on subsequent calls:

// App-specific authentication state.
class PrintServiceAuthenticationState
{
public:
PrintServiceAuthenticationState() : mAuthenticated(false)
{
}
bool mAuthenticated;
std::string mClientUsername;
};
// Servant binding access control.
bool onPrintServiceAccess(int methodId)
{
PrintServiceAuthenticationState & authState = session.getSessionObject<PrintServiceAuthenticationState>(true);
if (!authState.mAuthenticated)
{
// Here we are checking that the client is using either NTLM or Kerberos.
if (tp == RCF::Tp_Ntlm || tp == RCF::Tp_Kerberos)
{
authState.mAuthenticated = true;
authState.mClientUsername = session.getClientUserName();
}
}
return authState.mAuthenticated;
}

This example uses the access control callback to inspect the transport protocol the client is using, and uses that to determine the identity of the client.

In some situations you may want the client to provide extra authentication information, beyond what is available though the transport protocol. This typically means having the equivalent of a Login() method on the interface, that needs to be called before any other method on the interface. The access control callback can be used to verify that Login() is called before any other method:

// App-specific login info.
class LoginInfo
{
public:
// ...
void serialize(SF::Archive & ar)
{}
};
RCF_BEGIN(I_PrintService, "I_PrintService")
RCF_METHOD_V1(void, Login, const LoginInfo &)
RCF_METHOD_V1(void, Print, const std::string &)
RCF_END(I_PrintService)
// App-specific authentication state.
class PrintServiceAuthenticationState
{
public:
PrintServiceAuthenticationState() : mAuthenticated(false)
{
}
bool mAuthenticated;
std::string mClientUsername;
LoginInfo mLoginInfo;
};
// Servant object.
class PrintService
{
public:
void Login(const LoginInfo & loginInfo)
{
PrintServiceAuthenticationState & authState = session.getSessionObject<PrintServiceAuthenticationState>(true);
if (!authState.mAuthenticated)
{
if (tp == RCF::Tp_Ntlm || tp == RCF::Tp_Kerberos)
{
authState.mAuthenticated = true;
authState.mClientUsername = session.getClientUserName();
authState.mLoginInfo = loginInfo;
}
}
}
void Print(const std::string & s)
{
std::cout << "I_PrintService service: " << s << std::endl;
}
};
// Servant binding access control.
bool onPrintServiceAccess(int methodId)
{
if (methodId == 0)
{
// Calls to Login() are always allowed through.
return true;
}
else
{
PrintServiceAuthenticationState & authState = session.getSessionObject<PrintServiceAuthenticationState>(true);
return authState.mAuthenticated;
}
}

In this case, the access control callback allows calls to Login() to go through, while for any other method on the I_PrintService interface, it checks for the existence of the authentication state that the Login() call creates.

The Login() method is identified in the access control callback by its method ID - in this case 0, as it is the first method on the interface. Method ID's are assigned in incremental order, from 0, so if for example Login() had been the third method on the interface, it would have had a method ID of 2.

Server Objects

Server objects are application specific objects that are created by your server-side code and stored in the RcfServer. Unlike session objects (which are stored in RcfSession), server objects persist and are accessible outside of the RCF session in which they were created. The lifetime of server objects is managed by RCF::RcfServer, using a garbage collection policy.

Server objects are manipulated with the following functions:

Here is how we would create and update a server-wide session object for authenticated users:

class PrintServiceSession
{
public:
std::string mUsername;
std::uint32_t mMessagesPrinted = 0;
};
typedef std::shared_ptr<PrintServiceSession> PrintServiceSessionPtr;
// Servant object.
class PrintService
{
public:
void Print(const std::string & s)
{
std::cout << "I_PrintService service: " << s << std::endl;
RCF::RcfServer & server = session.getRcfServer();
std::string clientUsername = session.getClientUserName();
if ( clientUsername.size() > 0 )
{
std::uint32_t gcTimeoutMs = 3600 * 1000;
PrintServiceSessionPtr printSessionPtr = server.getServerObject<PrintServiceSession>(
clientUsername,
gcTimeoutMs);
if ( printSessionPtr->mUsername.empty() )
{
printSessionPtr->mUsername = clientUsername;
}
++printSessionPtr->mMessagesPrinted;
}
}
};

This code creates a PrintServiceSession object for each authenticated user. The PrintServiceSession object will be shared across all connections associated with that authenticated user.