Why?
Recently the Twitterverse has been abuzz with discussion about the drawbacks of GraphQL, the benefits of regular old REST, and how amazing tRPC is. By default, because my backends are often written in Rust, I've been using GraphQL. And for the most part it's OK, but recently I've been wondering if we can't do better. And that has led me to gRPC or gRPC Remote Procedure Calls*(It's a recursive acronym!)*, the seemingly little known API framework used by such big names as Uber, Square, Docker, Cisco, CockroachDB, Spotify, and Google.
gRPC offers high throughput, low latency communication over HTTP/2, is very lightweight, offers bi-directional streaming, and 7-10x performance improvements over a REST+JSON implementation. It also generates language bindings from a .proto
file with it's own IDL, and is completely open source. All in all, it sounds a LOT like Prisma, but with some nice perf benefits. Let's see how that works out.
Core Concepts
Remote Procedure Call (RPC)
A Remote Procedure Call is a method for building distributed systems. It allows one to define a function(also called a service in gRPC or a stub/skeleton) on one machine, and then for remote clients to call it as if it was a native function on the caller's device. Crucially, this does not mean that a local call is the same as a remote one, with additional processing to handle its operation over the wire. There are many different flavors of RPC, starting with Sun RPC in the 1980s(used to build NFS), and more modern examples like tRPC or gRPC. For gRPC, the functions, their inputs, and the possible outputs are all defined using Google's Protocol Buffers spec.
Protocol Buffers
For those unfamiliar, Protocol Buffers are a language neutral, platform agnostic and extensible mechanism for serializing structured data. One can define their API's functions, requests, and responses in a simple structured language. It has a wide variety of possible scalar values, to encode anything from a uint32
to bytes
or a String
. This gives some nice flexibility over GraphQL.
When sent over the network, the data is serialized into a binary form, and deserialized at the server. The message format is not self-descriptive, both servers and clients must have the same protocol buffer definition. Special care must be taken not to replace existing messages or field order if there's a chance of schema mismatch.
Bonus points if you can tell who wrote the protocol buffer spec from the logo ;).
The "you get i32 or f64 for numbers" is a graphql decision unfortunately
— Sage Griffin 🏳️⚧️🏳️🌈 (@sgrif) September 12, 2022
Here's an example .proto
file where we can define a protocol.
syntax = "proto3"; package helloworld; service Greeter { rpc SayHello (HelloRequest) returns (HelloReply); //unary rpc LotsOfReplies(HelloRequest) returns (stream HelloResponse); //Server side streaming rpc LotsOfGreetings(stream HelloRequest) returns (HelloResponse); // Client Side streaming rpc BiDiGreetings(stream HelloRequest) returns (stream HelloResponse); // bidirectional } message HelloRequest { string name = 1; float decimal = 2; bytes buffer = 3; } message HelloReply { string message = 1; }
Here is an example "protobuf" file from tonic's simple hello world example.
Preamble
The first two lines are used by Tonic to determine details about the Protocol Buffers.
syntax="proto3"
tells us that it is using v3 of the Protocol Buffer spec, and package helloworld
tells Tonic the name of the package that we can import later.
Service Methods
gRPC has four "service methods", or types of functions in our api.
- Unary RPCs - Denoted by the
rpc
keyword, these are typical functions where a single request is sent to the server and a single response is returned. SayHello in the above example is a unary RPC. - Server Streaming RPC - Has the
rpc
keyword, and returns astream
. This allows the server to send a stream or multiple messages in response to a request. The client reads from the stream until there are no more responses, and the message order is preserved. LotsofReplies is a Server Streaming RPC. - Client Streaming RPC - Has the rpc keyword, and takes a stream as an input. The client sends a stream of messages and the server returns a single response. LotsOfGreetins is a Client Streaming RPC
- Bidirectional Streaming RPC - Has the
rpc
keyword, and takes astream
as an input and an output. Both sides send a sequence of messages using a read/writestream
, in whatever order the client/server would like. BiDiGreetings is a Bidirectional Streaming RPC.
gRPC is unique in that a deadlines can be set on the client and a timeout on the server, a request can be cancelled at any time, and
More detail about these methods can be found in the gRPC docs.
message HelloRequest { string name = 1; float decimal = 2; bytes buffer = 3; } message HelloReply { string message = 1; }
Messages
These are request and response objects passed into and returned from service methods. They are numbered, and serialized before being passed to the server. HelloRequest
and HelloResponse
are messages with a string type, check out this list for all the types! While the Rust types aren't listed in this table, they are a fairly straightforward mapping from the included C ones.
Metadata
Metadata are key value pairs that can pass additional data to the gRPC server or client, and are opaque to gRPC itself. For example, authentication tokens can be passed as metadata from client to server. These are not usually defined in the .proto
file and are handled in your resolvers.
Channels
A channel provides a connection to a specific gRPC server and port. Channels have different configuration options when they're opened, and accept metadata as well. See your gRPC server docs for available options.
Compilation
Protobuf files are compiled into language bindings with a language dependent compiler. For Rust that compiler is called prost
, a handy Rust wrapper around the Protocol Buffers library. Other languages might use protoc
or their own specific implementation.
Interoperability
One of the neat things about Protocol Buffers is that the file can be reused for all the other clients and servers in your model. If you have an IOS app, you can pass it your server's .proto
file and write your own client functions. Or a web site, or another server, or a microcontroller. Possibilities abound! For Rust, Tonic, can handle generating both server and client bindings in the same crate.
Time for an Example!
Now that we know the basics, let's use it to build a gRPC server in Rust with Tonic.
gRPC API development follows 2 basic steps
- Define your messages(Service method inputs and responses) and service methods(functions) in your
.proto
file. These will get compiled into Rust language bindings using prost. Other languages will use protoc or their own implementation - Write your resolvers in your language of choice.
If you're familiar with the GraphQL world, this would be a schema-first approach. To my knowledge, there are no code-first gRPC packages, but it's broad language support suggests there could be something out there.
Hello World in gRPC
I thought about writing my own version of a hello world in gRPC, but ultimately I discovered there wasn't much to add over the existing resources. Check out the links below for gRPC Hello World examples in your language of choice
Once you have the server up, you should be able to query it using Postman, who has announced native support, or an open source alternative called BloomRPC. If you're a curl lover, there's also grpcurl.
I recommend checking out the official gRPC website for other resources and additional info!
gRPC-web
Up to this point, I hope I've enticed you into trying gRPC, but unfortunately it's not all sunshine and roses. gRPC works great if you are communicating between two gRPC servers/clients(like a mobile app and an API server, or a file server and an API server), but it has some limitations in the browser itself. Primarily this is because browsers do not currently have support for streaming from the client, and somewhat limited HTTP trailer support. Check out this issue for progress on that front. To work on that, Google is developing gRPC-web, which has a bit more limited feature set.
It only supports unary and server streaming RPCs, and there needs to be a proxy of some sort running between your gRPC server and your browser client. The typical choice for that is Envoy. In Rust, there's a handy tonic-web
crate that eliminates the need for the proxy. Other languages may have similar solutions, or a grpc-web native server solution.
Conclusion
gRPC seems to be little known among the web development community, but has widespread adoption among large companies for server/app/mobile communications. With the advent of gRPC-web, I believe it's a viable option to replace REST or GraphQL in applications that desire high throughput, low latency, and language flexibility. Its ability to handle streaming in different configurations suggests it might be a viable replacement for Web Sockets as well.
I'm excited to try it on some of our sites, and maybe on some embedded devices(ESP32 anyone?). If I've made any mistakes, or you want to show off something you've built using gRPC, or anything else, feel free to reach out on Twitter! I love hearing from y'all