Have you ever used a Google SDK, e.g. Firebase on a client or backend? Do you want to know more about the underlying technology? Let’s learn more about the basics of gRPC, a modern high-performance multi-platform RPC framework. We will discuss the types of communication, how it affects your API design and how known concepts translate to gRPC.
When starting with gRPC, it can be challenging to answer what it actually is. A transport protocol, a software framework, a methodology? Officially it is an “RPC framework”, but when you read ”framework”, think about it in a more general sense, not like the Ruby on Rails framework. The best way to set up our expectations for now is to think of it as an alternative to REST and GraphQL.
gRPC is a web API design
RESTful API design centers around resources and a finite set of defined operations – “methods” (get method on resource collection lists, update method on resource detail changes it, …). GraphQL solves the problem of resource shape and its relations (a graph definition the user can query), limited operations (custom mutation definition), error codes and more. It even describes a protocol where simple request-response communication is not enough (subscriptions).
While both approaches proved flexible for many scenarios by the test of time, an alternative RPC design allows the interface to be more versatile. The server defines services with custom procedures that the clients can remotely invoke. It is by no means a revolutionary concept, you might be familiar with this approach from “older” technologies like XML-RPC, SOAP or RMI.
REST and GraphQL designs give you more guidelines and rules, which is usually a great thing, but sometimes it slows you down or forces you to go against them (take a second to think of a purist RESTful API design for user authentication). With RPC, you have more freedom to model the API, making it easier to fit it to your domain, but it is more difficult to keep your design consistent. It removes the friction of the communication protocol and allows you to call the web API almost as if you were calling a function in your code.
Let’s look at an example.
gRPC is a communication protocol
Ok, we need custom services and custom-defined procedures. These are typically defined in Protocol Buffers: a language for defining a schema for messages ,services and mechanism for serialization. Here is a simplified schema of a service:
message GetCatRequest { string name = 1; }
message Cat { string name = 1; }
service CatService {
rpc GetCat (GetCatRequest) returns (Cat);
}
The schema describes all required information for both the client and the server to communicate.
- The client knows which services (service CatService) and procedures (rpc GetCat) are available
- Request (GetCatRequest) and response (Cat) messages are known from the schema
- Messages are defined independently of the server or client implementation and describe the format with data types, making the schema a recipe for serialization and deserialization
Data types
It only makes sense for the primitive data types to reasonably cover the broader needs, even for e.g. systems language. That is why you can find both signed and unsigned variants of integers and you are forced to specify the size variant (e.g. uint64). As for the non-scalar types, apart from defining your messages that can be nested, you have several composite types, such as enum, maps, unions, a repeated modifier for collections and several official messages for timestamp, duration etc.
Call types
In the example, we only saw a “request-response” RPC type. However, there are altogether 4 types of communication:
message GetCatRequest { string name = 1; }
message WatchCatsRequest {}
message FeedCatsRequest { string food = 1; }
message ShareLocationRequest { int32 lng = 1; int32 lat = 2; }
message ShareLocationResponse { float travelled_meters = 1; }
service Cat {
rpc GetCat (GetCatRequest) returns (Cat);
rpc WatchCats (WatchCatsRequest) returns (stream Cat);
rpc ShareLocation (stream ShareLocationRequest) returns (ShareLocationResponse);
rpc FeedCats (stream FeedCatsRequest) returns (stream Cat);
}
- Unary GetCat: The client sends GetCatRequest and receives a Cat
- Server streaming WatchCats: Client sends WatchCatsRequest (empty, they are just happy to watch some cats) and server sends a stream of Cat objects through the open connection. The timing is on the server, it can send a hundred cats right away in a busy hour, send one each minute or not send any at all if there is none around. Both client and server can end the call.
- Client streaming ShareLocation: The client (a cat in this case) opens a stream and can send location updates (for example, as it wanders through the neighborhood). This is the “inverse” of server streaming. When the client ends the call, the server can react with a response, for example sending the statistics on how long the journey was.
- Bidirectional streaming FeedCats: This call is a combination of both streaming options. Client initiates the call and can both send and receive messages. Sometimes you just throw food to feed the cats, but they don’t come. Sometimes you have a single morsel of tuna and two cats show up or you have nothing at all and seven cats are already there waiting for the nibble.
Additional properties
Atop of the communication means described, there are also some concepts you are familiar with
- status codes
- and metadata, which is a counterpart to HTTP headers in every way you can think of (they are actually also transported in HTTP headers). There are a few types of metadata, but the difference becomes really important only when you use non-unary call types, which is rarely the case.
Standard services
There are a few common domain-independent problems people generally solve when implementing a web API, that is why there are standards for that as a part of the framework.
- Is the server or service running? See GRPC Health Checking Protocol
- What is your schema? See GRPC Server Reflection Protocol
gRPC is a transport protocol
As we already know now, communication is not just about messages coming in and out. If you inspect the HTTP activity of a gRPC communication, you will probably see some requests and responses with Content-Type: application/grpc+proto. These messages (in brief) contain the binary serialized Protocol Buffers messages and status codes in payload and metadata (in headers and payload).
The streaming features are actually implemented via HTTP/2 streams, which is basically great news unless
- There is poor support of HTTP/2 for you platform (this is most probably not the case)
- One of your clients is a web client.
You are unlikely to face technological issues with HTTP and gRPC on server, desktop or app technologies, because you are typically provided full access to the network. This is not the case for the web, where your code obviously cannot have full access to anything for security reasons and must interact with the system through a set of APIs. This control does not allow fine-grained control over HTTP/2 frames within a stream and therefore full gRPC support is not possible at this time for the web.
Sacrificing client-side & bi-directional streaming call types, you can connect your web clients through a sidecar proxy, typically Envoy.
More nerdy details regarding the transport can be found in the specification.
gRPC is a “framework” you install and use
We have covered many parts of “what gRPC is”. Finally, it is a set of tools and libraries for various platforms you should use for gRPC communication.
The tools
The communication is handled by software available for supported platforms. Implementations usually offer both dynamic (invoke RPC with dynamic name) and static invocation (invoke RPC on a generated object). Usually, on a larger project, you will be using the static invocation to get the benefit of type checking against your schema. Codegen tools helping you with stub generation will be the most important ones you will use.
The library
After the setup, your code will typically invoke generated stubs, as seen on the following figure:
Let’s take a look at a simple code example. The following TypeScript server code uses proto.cat framework for brevity and showcases a simple unary RPC call implementation on the server and client invocation. This could be either a web browser (though there are some complications that were already mentioned) or another server: two backend services communicating together.
import { ProtoCat } from 'protocat'
// Generated service definition
import { CatService } from '../dist/cat_grpc_pb'
app = new ProtoCat()
app.addService(CatService, {
getCat: async call => {
const cat = await getCatByName(call.request?.getName() ?? '')
call.response.setName(cat.name)
.setHealth(cat.health)
.setLevel(cat.level)
.setClass(cat.profession ?? 'warrior')
}
}
app.start('0.0.0.0:3000')
// Generated client
import { CatClient } from '../../../dist/test/api/v1/cat_grpc_pb'
// Generated message
import { Cat } from '../../../dist/test/api/type/cat_pb'
import { ChannelCredentials } from '@grpc/grpc-js'
const client = new CatClient(ADDR, ChannelCredentials.createInsecure())
const cat = await new Promise<Cat>((resolve, reject) => {
client.getCat(new GetCatRequest().setName('Proto'), (err, res) =>
err ? reject(err) : resolve(res)
)
})
console.log(`[${cat.getClass()} ${cat.getLevel()}] ${cat.getName()}`)
Q&A
Let me try to quickly address some questions that might arise, if you’re new to gRPC.
How to invoke an RPC?
The tooling got better, but being a binary protocol, you need server reflection or linking the schema, so it’s a bit trickier than REST or GraphQL. There is CLI grpcurl, or GUI BloomRPC (which does not support reflection and response metadata) and my recommendation: Wombat. Recently Postman added support for gRPC, but maintaining its current workflow, which forces you to create a “request” object for every call, is just burdensome and does not really fit the RPC model.
How to distribute schemas?
For “internal” API, we went with syncing the proto files through cloning a git repository, which works ok for developers, but gets complicated when you want to drag in PM, QA, third parties etc. Luckily, I ported the server reflection for Node.js, so you can use it with any JS gRPC server. The trouble still is the public environment, where you probably don’t want to have it enabled. Sharing schema files is still probably the best option there.
Best practices recommendations?
There are a ton of resources for REST and GraphQL, but not that many for gRPC, since there are not so many people using it. For the first API we did, we were just learning a lot and did not pay too much attention to conventions. After revaluation, we studied and followed Google’s API design guide, which was an invaluable resource for keeping the API schema maintainable, extendable, consistent and for avoiding breaking changes.
Static analysis of the schema?
We’ve been using Uber’s Prototool, because its unofficial successor Buf was not ready to give us all the rules we were used to from Prototool. It might be worth checking out now, since Prototool is not actively maintained anymore.
How’s proxy support?
For a long time, there wasn’t any HTTP proxy that supported gRPC. In 2020, Cloudflare announced beta support and it is now available as a stable feature. If you’re wondering, why is it such a big deal to support gRPC since Cloudflare is already capable of proxying HTTP2, read more in their blog, the answer is very similar for web clients still needing grpc web, even though browsers generally support HTTP2. To support web clients via grpc-web protocol, Envoy is still your best shot.
How to handle caching?
Server caching is still challenging. HTTP caching is complicated since all requests are POSTs and e.g. Cloudflare support is a single toggle, without any additional configuration. We have a custom application solution for general request-response caching, which we’re excited to share more about (but if you’re really interested, it’s implemented as a part of ProtoCat
Have more? Do let us know.
Summary
We’ve covered different layers of what the gRPC ecosystem offers which should give you a general understanding of the framework.
There is more we can share about our experience with running gRPC in production (and maintaining it for nearly three years), reach out to us or leave a comment if there is a topic you want to learn more about.