This documentation describes implementation details of the low-level components of the RPC mechanism.
Requirements
- ability to perform method calls of remote services
- RPC mechanism must work without meta-mechanism, instead the reflect mechanism should be used.
- for the methods parameters and return value the existing serialization concept is used
- services and methods are addressed by their names
- different kinds of RPC calls must be possible (binary rpc calls, JSON rpc calls, etc)
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
- each RPC call is identified by a unique ID (based on a random UUID)
- all informations of the RPC call are stored in request and response objects, they act as containers. The content of the request and response objects can be transmitted from client to server and vice versa (the transmission is not part of the low-level RPC mechanism)
- to support different kinds of RPC calls (binary, JSON, etc) different request and response objects are provided for each RPC kind by a backend. The backend is specialized for a certain kind of RPC calls
- most RPC backends distinguish between server side and client side request and response, hence most backends provide four classes: ClientRequest, ClientResponse ServerRequest and ServerResponse.
The invokation of an RPC call is described below:
- 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.
- the data of the RPCRequest generated by the call method is shipped as RPCClientRequest to the remote side
- the RPCServer receives the RPC request as RPCServerRequest from the client
- 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
- the server will return the result as RPCServerResponse
- the response is received as RPCClientResponse at the client side
- 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:
- a client-side request
- a corresponding server-side request
- a server-side response
- a corresponding client-side response
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:
concept ClientRequest
{
typedef ... Response;
std::string generateCallID();
void setHeader(const std::string& callId, const std::string& service,
const RPCSignature& signature);
template <typename P>
void setParameter(const P& param);
};
concept ServerRequest
{
typedef ... Response;
void getHeader(std::string& oCallId, std::string& oService);
bool checkSignature(const RPCSignature& signature);
const RPCSignature& getSignature();
template <typename P>
void getParameter(P& oParam);
};
concept ServerResponse
{
void setHeader(const std::string& callId);
void returnException(const std::exception& ex);
template<typename R>
void returnResult(const R& res);
void returnVoid();
};
concept ClientResponse
{
void getHeader(std::string& callId);
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.