Gee, RPC!
Introduction
“Woah! I came in here looking for some nerdy info about gRPC - that fancy pants RPC framework everyone is talking about these days. But who is this CUTE monster above?”
Isn’t he the cutest? The cute monster is gRPC’s mascot, Pancakes! Say Hello 🐶
Pancakes is here with a simple introduction to gRPC. This is his first time - so if you find any mistakes, please raise an issue here and we’ll get it fixed stat! Woof!
What is RPC and why is it needed?
Current scenario
As of today, the vast majority of web APIs on the internet are based on HTTP + JSON, with REST as the architectural principle of how these APIs are designed. HTTP/REST has excellent implementations in every conceivable programming language out there and is extremely popular, but it isn’t without it’s problems. Some of the major drawbacks of HTTP/REST are:
- The client program (the caller) needs to build extra tooling 🔧 for every new REST API it has to interact with to construct the request headers and payload. Or expect the REST API to come with a client library 📒 (in the same language as the client’s implentation) so the functionality can be integrated easily.
- No formal machine-readable contract 🤖. This makes API discovery impossible and writing client-libraries (for every programming language to be supported) a manual job 🤢.
- Streaming is almost impossible ❌.
- JSON is a text-based representation, making it extremely bulky/inefficient 🐢 for transmission over network.
- A pure REST-ful paradigm can’t model all the capabilities to be supported by the API 👎 (example: restarting a machine).
What are the alternatives?
There are several, but in the current scope, we look at RPC. RPC (Remote Procedure Call) is a request-response protocol.
In Layman terms, RPC allows a program to call, say, a method or an API that could be running anywhere on a remote machine as if it were part of the same program (i.e. using standard language semantics without having to even think about network configurations) and get back a response. Wikipedia has a more formal definition, if interested.
Welcome gRPC
gRPC is a modern open source high performance RPC framework that can run in any environment. It can efficiently connect services in and across data centers. gRPC evolved out as the V2 of Google’s internal RPC solution - Stubby. gRPC solves all the problems listed above (and more!).
Some of the key features that makes gRPC stand-out are:
- Idiomatic client libraries in 10 languages
- Highly efficient on wire and with a simple service definition framework
- Bi-directional streaming with http/2 based transport
- Pluggable auth, tracing, load balancing and health checking
Dive into gRPC
Service definition
gRPC, like many RPC systems, is based around the idea of defining a service, specifying the methods that can be called remotely with their parameters and return types.
By default, gRPC uses protocol buffers as the Interface Definition Language (IDL) for describing both the service interface and the structure of the payload messages. It is possible to use other alternatives if desired.
Protocol Buffers
Protocol Buffers, or Protobufs, are Google’s language-neutral, platform-neutral, extensible mechanism for serializing structured data – think XML, but smaller, faster, and simpler.
Protobufs provide type-safety, prevent schema violations, provide simple accessors, backward compatibility and extremely fast serialization/deserialization capabilities.
*Stats from JSON vs Protocol Buffers vs FlatBuffers by Karthik Khare
For protobufs, you define how you want your data to be structured once. Then, protobuf’s compiler (protoc
) can compile them to source code in a variety of languages to easily write and read your structured data.
A simple service definition with 2 Unary RPCs for a telephony API might look like this:
syntax = "proto3";package calls;
service Calls { rpc Dial(DialReq) returns (Call) {} rpc Get(GetReq) returns (Call) {}}
message DialReq { string to = 1; string from = 2; string callback_url = 3;}
message GetReq { string sid = 1;}
message Call { string sid = 1; string to = 2; string from = 3; string status = 5; int64 created_at = 6; int64 started_at = 7; int64 duration = 8;}
Client-Server communication
Once the API Spec is formally defined in .proto
files, two things are possible:
Since this is a machine-readable contract, the protoc
compiler can generate a client-library for this API in any of the 35+ supported languages! It is then possible to start interacting with the generated library right away with very minimal code. For instance, in the above example, the library can be generated with:
protoc --protoc_path=. --js_out=./js --grpc_out=./js --plugin=protoc-gen-grpc=`which grpc_node_plugin` calls.proto
And the below code is a fully-working Node.JS client intercting with the generated library (calls_grpc_pb.js
).
var calls = require("./calls_grpc_pb");var grpc = require("grpc");
var calls = new calls.CallsClient( "lolcahost:50051", grpc.credentials.createInsecure());var req = new calls.GetReq();req.getSid("CA39c33a11fed4a5a1c01");calls.get(req, function (err, resp) { console.log("Got call:", resp.getFrom(), resp.getTo());});
gRPC can also generate server stubs in 10 different languages! For instance, here’s working code implemeting the interface exposed by the generated stub:
package main
import ( "_")
type CallService struct { calls map[string]*calls.Call}
func (s *CallService) Dial(ctx context.Context, req *Calls.DialReq) (*calls.Call, error) { sid := id.New() call := &calls.Call(Sid: sid, From: req.From, To: req.To, CreatedAt: time.Now().Unix()) s.Calls[sid] = call // TODO: Instantiate the call return call, nil}
func (s *CallService) Get(ctx context.Context, req *calls.GetReq) (*calls.Call, error) { call, ok := s.calls[req.Sid] if !ok { return nil, fmt.Errorf("No call found with SID '%s'", req.Sid) } return call, nil}
The above generated stub is essentially an interface for the API contract.
On the server side, the server implements the methods declared by the service and runs a gRPC server to handle client calls. The gRPC infrastructure decodes incoming requests, executes service methods, and encodes service responses.
On the client side, the client has a local object known as stub (for some languages, the preferred term is client) that implements the same methods as the service. The client can then just call those methods on the local object, wrapping the parameters for the call in the appropriate protocol buffer message type - gRPC looks after sending the request(s) to the server and returning the server’s protocol buffer response(s).
That’s it. True magic does exist! (but also demystified in this talk at KubeCon EU 2018 🎩).
Streaming
gRPC also supports streaming in both directions - between server and client. This can be very easily defined in the proto defition itself with the stream
keyword before the payload or response, as appropriate.
Unary RPCs
The client sends a single request to the server and gets a single response back, just like a normal function call.
rpc SayHello(HelloRequest) returns (HelloResponse){}
Server streaming RPCs
The client sends a request to the server and gets a stream to read a sequence of messages back. The client reads from the returned stream until there are no more messages. gRPC guarantees message ordering within an individual RPC call.
rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse){}
Client streaming RPCs
The client writes a sequence of messages and sends them to the server, again using a provided stream. Once the client has finished writing the messages, it waits for the server to read them and return its response. Again gRPC guarantees message ordering within an individual RPC call.
rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse){}
Bidirectional streaming RPCs
Both sides send a sequence of messages using a read-write stream. The two streams operate independently, so clients and servers can read and write in whatever order they like: for example, the server could wait to receive all the client messages before writing its responses, or it could alternately read a message then write a message, or some other combination of reads and writes. The order of messages in each stream is preserved.
rpc BidiHello(stream HelloRequest) returns (stream HelloResponse){}
Other useful concepts
Interceptors 👮
Interceptors in gRPC are essentially what is commonly called ‘middleware’. Interceptors allow for custom actions to be written that can happen during the lifespan of an RPC call. Simple examples can be handling the incoming payload and logging it to the console, for debugging purposes. But the usefulness of interceptors go much beyond that.
Deadlines ⏰
gRPC allows clients to specify how long they are willing to wait for an RPC to complete before the RPC is terminated with the error DEADLINE_EXCEEDED
. On the server side, the server can query to see if a particular RPC has timed out, or how much time is left to complete the RPC.
Cancellations 🚫
Either the client or the server can cancel an RPC at any time. A cancellation terminates the RPC immediately so that no further work is done. It is not an ‘undo’: changes made before the cancellation will not be rolled back.
Wrapping up
gRPC is an extremely easy to use framework wrapping a very complicated protocol, with tons of optimisations to max out it’s performance under the hood. If you have still any questions, please reach out to me via email or Twitter.