During my latest mentoring session with coworkers I was asked to explain how modern RPC frameworks work under the hood. What better way to illustrate this than to build one from scratch using components I blogged about previously (event objects, serializer framework, and TCP sockets).
Although not part of the video recording (which I will share later in this post) I explained that RPC frameworks generally work in two ways:
- The client and server code is created at runtime and can be built on demand. Good example of this is the XML-RPC-C library: it contains no RPC IDL nor associated compiler/code-generator. It is simpler of the two.
- The RPC methods of the server and associated client code is defined in form of an IDL, then ran through a code-generator producing almost all of the necessary client and server code. gRPC and Thrift are prime examples of this: both come with code generators and are very sophisticated. This was the type of framework I was going to talk about.
I did not design an IDL nor did I build a parser and code-generator for my students; there wasn’t nearly enough time for that. What I wanted to illustrate were different components which would have been generated by a modern RPC framework and how they would fit together. Starting with the client interface and code created to connect and issue calls to the server, serialization of parameters, network transport, command parsing and dispatching on the server side, actual command logic (this part would not be generated by a framework), and finally everything needed to send responses back to the client.
The starting point of creating a simple calculator server capable of adding and subtracting numbers would have been the server’s interface declaration using some form of an IDL:
1 2 3 4 5 |
interface Calculator { int Add(int, int); int Sub(int, int); } |
This would then be processed by the framework’s generator to produce actual C++ code (or any other language) that would allow the programmer to easily connect, issue calls, and receive responses from the server:
1 2 3 |
auto c = RPCClient(server_host_name, server_port); auto a = c.Add(2, 2); auto s = c.Sub(10, 5); |
Creating instances of the RPC server would be equally easy, but would also require providing the core logic of the server procedures:
1 2 3 4 |
auto s = RPCServer(listening_port); s.SetAddHandler([](int x, int y) { return x + y; }); s.SetSubHandler([](int x, int y) { return x - y; }); s.Run(); |
In order for this to work a lot more code would have been generated then compiled against the static parts of the RPC framework: serialization, network transport, compression, encryption, authentication code, etc.
During my class I implemented the parts that would have been generated based on the desired set of procedure calls (the IDL), and reused code I developed for this blog in the past in place of the framework’s static parts.
Keep in mind that some of the supporting code I have already prepared before recording the class and I had it open on another screen, but the IDL based code I was coming up with on the fly, aka freestyling it 🙂
I figured it’s worth a mention in case my delivery appears less than… perfect.
Reworked and cleaned up code from the class:
Protocol: lrpc_proto.hpp. Client: lrpc_c.cpp. Server: lrpc_s.cpp.
Serializer: lsf.hpp. Network: socket.hpp. Event: event.hpp.