Remote Procedure Call Implementation
The definition files
Include all you definitions: custom types, services, methods, in .rpc files. The extension .rpc is optional.
The parser
This is an application that knows how to parse .rpc files and generates:
- for C++:
- custom types classes
- for server side: a stub class for every service, containing service methods as pure virtual functions.
- for client side: an invoker class for every service. It knows how to encode/decode specific parameters and makes the call of remote methods straight and easy.
- for Javascript:
- custom types definition, as javascript objects.
- an invoker class for every service, making remote procedure call from javascript straight and easy.
The server
See the big pictures.
Server components
- the service implementation: rpc::ServiceInvoker
This is a common interface for all services. The parser will generate a specific service interface rpc::ServiceInvokerXxx for Xxx service. The programmer will implement the generated interface class. Keep in mind that service methods may be asynchronously called from various thread contexts. - the services manager: rpc::ServicesManager
Implements a map: service name → ServiceInvoker (which is a service instance). It receives queries, looks up the service name, and passes the query to the service implementation object. - the execution layer: rpc::IAsyncQueryExecutor
This is a common interface for all execution layer implementations:- execution by thread pool: rpc::ExecutionPool
Uses a pool of threads, on every thread there is an ExecutionWorker. The ExecutionWorker passes the query to the "services manager" which forward the call to the service implementation. So the actual rpc call execution takes place in worker's thread context. Parallel rpc call execution is very likely. - execution simple: rpc::ExecutionSimple
This class simply forwards the received query to the "services manager". So the actual rpc call execution takes place in server transport's thread context.
- execution by thread pool: rpc::ExecutionPool
When obtaining a response, the execution layer forwards the response to a rpc::ResponseReceiver (which is the transport).
- the transport layer.
Being a server like component, there are 2 main implementations here:
- the rpc::Server
Implements a TCP/SSL server. Listens on a given port and spawns rpc::ServerConnection objects.- the rpc::ServerConnection implements ResponseReceiver interface.
Reads rpc packets, partially decodes them into rpc::Query (decodes everything but call parameters, which are left as raw data), and forwards the query to the execution layer. The response will come back through the ResponseReceiver interface.
- the rpc::ServerConnection implements ResponseReceiver interface.
- the rpc::HttpProcessor implements ResponseReceiver interface.
Implements a http processor. Uses a pre-created http server, to listen on a given path, reads a http post containing an rpc packet, decodes it into rpc::Query and forwards the query to the execution layer. The response will come back through the ResponseReceiver interface, and sent back to client as http response.
- the rpc::Server
The client
See the big pictures.
Client components
- the wrapper: rpc::ServiceWrapper
This is a common interface for all service wrappers. It will be extended by the parser generated class (e.g. !RPCServiceXxx for service Xxx). The generated class has methods for easy invoking the remote service. It encodes the call params, then sends the service name, method name, and encoded call params to "the transport" along with a callback to get the response. For every service method, there are 2 invoking methods here:- synchronous invoking. Invokes and waits for answer.
- asynchronous invoking. The client application provides a response-callback.
The Wrapper sends the invoke to server and returns immediately.
The answer will be delivered to the response-callback.
Asynchronous calls can be canceled:
For asynchronous invoking, a CALL_ID is generated and returned to client application. The client application may cancel the response receiving by providing this CALL_ID.
- the transport: rpc::IClientConnection
Common interface for all implementations. For now there are 2 implementations:- by TCP/SSL: rpc::!ClientConnectionTCP
Implements a client TCP/SSL connection. Uses net::NetConnection for both TCP and SSL compatibility. - by HTTP: rpc::!ClientConnectionHTTP
Encodes the rpc packet inside a HTTP request, POST the request to server, receives the rpc answer as HTTP response.
- by TCP/SSL: rpc::!ClientConnectionTCP
The transport receives (from wrapper) service name, method name, raw params and a response-delivery-callback. Transport's job is to encode the rpc packet send it to server, get response, partially decode response (the result remains raw data), forward the response through the response-delivery-callback. The transport generates transaction IDs (XID). The wrapper may cancel the response receiving for a given XID.
Use case
Creating a simple service
Define the service and the involved types into simple.rpc file:
Type Car {
string name;
int hp;
}
Service CarVendor {
string VendorName();
int CarPrice(string car_name);
Car CreateCar(string car_name);
}
Execute the parser:
> rpc_parser -cc "fServerTypes=auto_types, fServerInvokers=auto_invokers"
This will generate files:
- auto_types.h // defines Car type
- auto_types.cc
- auto_invokers.h // defines ServiceInvokerCarVendor service stub
- auto_invokers.cc
Implement the ServiceInvokerCarVendor:
class RPCServiceCarVendor : public ServiceInvokerCarVendor {
void VendorName(rpc::CallContext<rpc::String>* call) {
call->Complete("my vendor name");
}
void CarPrice(rpc::CallContext<rpc::Int>* call,
const rpc::String* car_name) {
if ( *car_name == "Dacia" ) { call->Complete(2000); return; }
if ( *car_name == "Renault" ) { call->Complete(4000); return; }
call->Complete(-1); // we make a convention here: -1 means no such car name
}
void CreateCar(rpc::CallContext<Car>* call,
const rpc::String* car_name) {
if ( *car_name == "Dacia" ) { call->Complete(Car("Dacia", 40)); return; }
if ( *car_name == "Renault" ) { call->Complete(Car("Renault", 80)); return; }
call->Complete(Car("", 0)); // another convention for no such car name
}
}
Create a C++ application:
.... RPCServiceCarVendor my_vendor_service; // instantiate your CarVendor service implementation rpc::ServicesManager rpc_manager; // create a service manager rpc_manager.RegisterService(my_vendor_service); // make your instance public rpc::ExecutionPool rpc_executor(rpc_manager); // create an execution layer rpc::Server rpc_server(rpc_executor); // finally create the TCP server .... start the server through selector , and let it run .... ....
Creating a client that invokes the simple service
In C++
Execute the parser:
> rpc_parser -cc "fClientTypes=auto_types, fClientWrappers=auto_wrappers"
This will generate files:
- auto_types.h // defines class Car
- auto_types.cc
- auto_wrappers.h // defines class ServiceWrapperCarVendor, a helper for easy invoking CarVendor's methods
- auto_wrappers.cc
Create a C++ application:
....
rpc::ClientConnectionTCP rpc_tcp_connection;
ServiceWrapperCarVendor car_vendor(rpc_tcp_connection);
// rpc::ClientConnectionHTTP rpc_http_connection; // if you like HTTP
// ServiceWrapperCarVendor car_vendor(rpc_http_connection);
rpc::CallResult<rpc::String> vendor_name_result; // will store the result of invoking VendorName
car_vendor.VendorName(vendor_name_result); // invoke the remote method, this will block until a response is received, or timeout fires.
if ( vendor_name_result.success_ ) {
LOG << "Vendor name is: " << vendor_name_result.result_;
} else {
LOG << "Failed to invoke VendorName, error: " << vendor_name_result.error_;
}
rpc::CallResult<Car> create_car_result; // will store the result of invoking CreateCar
car_vendor.CreateCar(create_car_result, "Dacia"); // invoke the remote method, with parameter "Dacia"
if ( create_car_result.success_ ) {
// A Car was created, but is it a good one, or the Car("", 0) bad one?
if ( create_car_result.result_.hp.Get().Get() > 0 ) {
LOG << "Car created: " << create_car_result.result_;
} else {
LOG << "CreateCar failed: No such car name.";
}
} else {
LOG << "Failed to invoke CreateCar, error: " << create_car_result.error_;
}
....
In Javascript
Execute the parser:
> rpc_parser -js "fTypes=auto_types, fWrappers=auto_wrappers"
This will generate files:
- auto_types.js // defines object Car
- auto_wrappers.js // defines ServiceWrapperCarVendor, a helper for easy invoking CarVendor's methods
Create a Javascript test:
....
function AsyncReturnCarPrice(call_status, result) {
if ( call_status.success_ ) {
alert("Car price is " + result + " and " + call_status.userData_); // userData_ is "we asked about Renault"
} else {
alert("CarPrice failed, error: " + call_status.error);
}
}
function AsyncReturnCreateCar(call_status, result) {
if ( call_status.success_ ) {
var car = result; // the result is a Car object
if ( car.hp > 0 ) {
alert("Car created: " + car);
} else {
alert("No such car, by name: " + call_status.userData_); // userData_ is "Dacia"
}
} else {
alert("CreateCar failed, error: " + call_status.error);
}
}
....
var car_vendor = new RPCServiceWrapperCarVendor(new RPCHttpXmlConnection("http://my_car_vendor_service_address"));
car_vendor.CarPrice("Renault", AsyncReturnCarPrice, "we asked about Renault"); // asynchronous invoke the "CarPrice" method. Result will be delivered to function AsyncReturnCarPrice. The last parameter will be passed to result function, helping you match call-reply.
car_vendor.CreateCar("Dacia", AsyncReturnCarPrice, "Dacia"); // another asynchronous invoke. No synchronous/blocking methods available for Javascript.
