MIRA
RPC (Requirements and Implementation Details)


This documentation describes implementation details of the low-level components of the RPC mechanism.

Requirements

Implementation Details

RPC Server

The Server is responsable for handling the server-side of an rpc call. The server stores all available services with its methods and invokes the requested calls. Each service is identified by its name. The service is formed of one or more service objects that provide the methods of the service. A service object is assigned using registerServiceObject(). The passed service object will then be reflected via its reflect() method to collect all methods and interfaces specified in this reflect method.

RPC Client

The Client is responsable for handling the client-side of an rpc call. The client is used to initiate an RPC call by calling its call() method. The call method takes the name of the service, the methods name and a variable number of parameters.

Mechanism

The invokation of an RPC call is described below:

  1. the RPCClient::call() method is called and generates an RPCRequest based on the desired service and method strings and the parameters of the method.
    • the RPCRequest can be a realization of multiple possible types, like: binary requests, json rpc request, etc.
    • the RPCRequest can be thought of some kind of container that contains all information about the desired call (for binary rpc calls it is a buffer with serialized data, with serialized parameters etc.)
    • the result of the RPCClient::call() method is an RPCFuture which allows the caller to wait until the call has finished and to access the result value of the call as soon as it becomes available.
    • the calls are identified by IDs which are generated randomly
    • internally a PendingResponse object is created for each call and stored in an internal map together with the call id. The PendingResponse object is kept until the RPCFuture of the call is destroyed (e.g. since the caller is not interested in the response) or until the actual response was received.
  2. the data of the RPCRequest generated by the call method is shipped as RPCClientRequest to the remote side
  3. the RPCServer receives the RPC request as RPCServerRequest from the client
  4. RPCServer::processCall() or RPCServer::processCallDeferred is called using the received request
    • these methods will examine the request and prepare the invokation of the requested method.
    • RPCServer::processCall() will call the method immediately, while RPCServer::processCallDeferred() returns a DeferredInvoker which can be stored and used to invoke the method call later
    • both methods take an RPCServerResponse object as second parameter. The result or error response of the call will be stored in this response right after invokation of the called method
    • similar to RPCClient::call() arbitrary types of RPCServerRequests and RPCServerResponse are supported (e.g. BinaryRPCServerRequest, BinaryRPCServerResponse, etc) which are provided by an RPC backend
  5. the server will return the result as RPCServerResponse
  6. the response is received as RPCClientResponse at the client side
  7. the RPCClient::handleResponse() method of the RPCClient is called with the received response. It will extract the return value (if any) and set that value to the RPCFuture in order to signal the caller that the desired call has finished and to return the result.
    • internally the RPCClient::handleResponse() method looks for the corresponding PendingResponse object (that was created in step 1) using the ID of the call, which is included in the response header
    • the PendingResponse object is responsable for handling the response

Backends

The RPC backends are used to decouple the RPCServer and RPCClient from the generation and parsing of the actual RPC requests and responses, since those will be different for different kinds of RPC calls like binary RPC calls, JSON RPC calls, etc.

Instead the RPC backends must provide four classes that encapsulate:

Objects of these classes are then passed to the RPCClient::call(), RPCServer::processCall()/ RPCServer::processCallDeferred and RPCClient::handleResponse() methods.

Note, that the four classes are counterparts. The data stream that is generated by the ClientRequest is parsed by the ServerRequest (after the server received the generated request stream). The data stream that is generated by the ServerResponse is parsed by the ClientResponse (after the client received the generated response stream).

Those classes must implement certain concepts, i.e. they must contain special methods that are called by the RPCClient and RPCServer class. The methods then generate or parse the requests or responses. The binary backend classes for example generate binary requests consisting of bit streams, while the JSON backend classes generate requests and responses in the form of JSON strings.

For each backend the four classes must implement the following concepts including all methods and typedefs:

// Client-side request.
concept ClientRequest
{
// The type of the corresponding CLIENT-side response of the same backend
typedef ... Response;
/*
Generates a unique call ID for a client request of this backend.
The call id must be returned as string although it can be restricted to
other datatypes like integers for the certain backend.
*/
std::string generateCallID();
/*
Write the request header consisting of the call ID, the service name
and the signature of the requested method.
This method is called first by the RCPClient when a new request is
generated.
*/
void setHeader(const std::string& callId, const std::string& service,
const RPCSignature& signature);
/*
Write the value of the next parameter to the request.
This method is called multiple times after the setHeader() method for
each parameter that is passed to the RPCClient::call() method.
*/
template <typename P>
void setParameter(const P& param);
};
// The Server-side request that corresponds to the above Client-side request.
concept ServerRequest
{
// The type of the corresponding SERVER-side response of the same backend
typedef ... Response;
/*
Read the request header including the call ID, the service name
and the signature of the method.
The call ID and the service name must be returned via the by-reference
parameters. The signature should be stored internally to be able to
handle the methods below.
This method is called first when a new request is handled by the RPCServer.
*/
void getHeader(std::string& oCallId, std::string& oService);
/*
Check, if the passed signature is compatible with the signature that was
read from the request header in the previous getHeader() call above.
*/
bool checkSignature(const RPCSignature& signature);
/*
Return the signature, that was read from the request header in the previous
getHeader() call above.
*/
const RPCSignature& getSignature();
/*
Read and deserializes the next parameter from the request.
This method is called multiple times for each parameter of the RPC method.
*/
template <typename P>
void getParameter(P& oParam);
};
// The server-side response, that is send after processing the above request
concept ServerResponse
{
/*
Write the response header consisting of the ID of the call.
This method is called first by the RCPServer to create a response
for a finished RCP call.
It will be followed by a call of either returnException(), returnResult()
or returnVoid().
*/
void setHeader(const std::string& callId);
/*
Write exception as result an RCP call.
This method is called after setHeader() in case of an error or
exception while processing the call.
*/
void returnException(const std::exception& ex);
/*
Write result of an RCP call.
This method is called after setHeader() in case of a successful
RCP call that returns a value.
*/
template<typename R>
void returnResult(const R& res);
/*
Write successful end of an RCP call.
This method is called after setHeader() in case of a successful
RCP call that does NOT return a value.
*/
void returnVoid();
};
// The client side response that correspons to the above server side respose
concept ClientResponse
{
/*
Read the response header (i.e. the call id of the RPC call)
from the response. This method is called first by the RPCClient when a
response is handled
*/
void getHeader(std::string& callId);
/*
Obtain return value from response and set it using promise.set_value()
or set exception using promise.set_exception().
See documentation of boost::promise and boost::future for details about
futures and promises.
This method is called after getHeader().
*/
template<typename R>
void getReturn(boost::promise<R>& promise);
};

For a reference implementation of these concepts see BinaryRPCBackend::ClientRequest, BinaryRPCBackend::ServerRequest, BinaryRPCBackend::ServerResponse and BinaryRPCBackend::ClientResponse.

Note, that the transmission of the data streams that are generated and parsed by the four concepts is not part of the low-level RPC mechanism. It must be implemented separately if the low-level RPC mechanism is used directly.