gRPC

Overview

gPRC is a high-performance open-source communication framework that was introduced in 2015 by Google. It’s built on top of the HTTP2 protocol and uses Protocol Buffers as the interface description language.

RPC

Using the Remote Procedure Calls (PRC) approach, a client application can directly call a method on a server application, even if they are hosted on separate machines and written in different programming languages.

The main idea is that a service specifies the methods which can be called remotely and declare its input/output parameters. Using such a way of communication, a client can follow the provided specifications and talk to the remote service within a known typed interface.

gRPC Communication Model

gRPC primarily uses a synchronous request-response model but can operate in asynchronous or streaming mode after the connection is established.

There are the modes that gRPC can work in:

  • Unary PRC
  • Server streaming RPC
  • Client streaming RPC
  • Bidirectional streaming RPC

gRPC has matured through its heavy battle-testing at Google, and many other major tech companies such as Square, Lyft, Netflix, Docker, Cisco, and CoreOS have adopted it.

The framework is widely used in a microservice architecture, where the application is broken down into small services communicating with each other over a network.

Architecture

gRPC organizes communication between the server and the client.

The client produces a local procedure call with the defined parameters sent to the server. Next, the client’s stub serializes the procedure invocation parameters using Protobuf and forwards the request to the local library. Finally, the OS makes a call with the serialized data to the remote gRPC server using the HTTP/2 protocol.

The server receives the request packets and calls the server stub procedure, decoding the data using Protobuf. The decoded data is passed to the stub implementation that processes the requested operation and sends back the encoded response to the client.

gRPC Architecture

👍 Advantages

  • Performance and Efficiency. On average, gRPC performs up to 10x faster than usual JSON APIs. The serialized binary messages are smaller and transferred via HTTP2 protocol with all its features. The binary format makes gRPC much more efficient compared to textual format protocols.

  • Schemas and validations. gRPC comes with Protocol Buffers definitions that consist of strict-typed schemas and interfaces for communication out of the box. There is no need to use third-party instruments to ensure stable and reliable API contracts.

  • Bidirectional streaming. Thanks to the HTTP/2 protocol, gRPC provides various types of streams that allow a client and a server to exchange real-time messages in both directions.

  • Language independent. gRPC is implemented for many popular programming languages such as Node.js, Java, Python, etc. A gRPC client and server are not required to use the same technology to talk to each other. The only condition has gRPC interface implementation is in place.

  • Code generation. Protocol Buffers compiler allows generating server stubs and message builders for every supported language, simplifying the creation of Client SDK on various platforms.

  • Built-in features. Many important things are already included, such as authentication, encryption, compression, load balancing, etc.

  • Security Because of HTTP/2 and binary messages format, gRPC ensures a secure data transfer. gRPC encourages the use of SSL/TLS to authenticate and encrypts data exchanged between the client and server.

👎 Disadvantages

  • Not suitable for public APIs. Most external consumers use the traditional REST approach. Also, gRPC is strict and does not provide enough flexibility that, for example, GraphQL does. External clients must have the .proto definitions to communicate with a gRPC-driven service.

  • Limited Browser Support. It’s impossible to call a gRPC service directly from a web browser. The only way is to use a proxy layer and gRPC-web.

  • Non-readable messages. All the messages are transferred in binary format, so to see message content, you need to use additional tools that allow working with the Protobuf payload.

  • Complex schema updates. As the gRPC’s compiler generates a codebase to work with defined schema, it might take additional effort in the case of radical schema changes. At the same time, once a schema is updated, the changes (represented via .proto files) must be shared with the rest of the components.

  • Relatively small community. The gPRC is not as popular as the other communication interfaces, so finding the correct answer for tricky issues may not be quick.

⚖️ Comparison

RESTful

FeaturegRPCJSON API
Language agnosticYesYes
HTTP versionHTTP/2HTTP 1.1*
Interface schemasProtobufThird-party tools**
Human-readableNoYes
Data transfer formatBinaryPlain Text
Data types supportRichBasic
Data transferring speedFastSlower
CommunityLowerLarge

* Traditional REST style uses HTTP 1.1, but it is still possible to take the HTTP/2. However, REST is not designed to use all the HTTP/2 features, and configuration will need additional effort.

** REST doesn’t come with any strict interface schemas and their validation. Instead, it requires a service definition tool like Swagger/OpenAPI.

Apache Thrift

Apache Thrift is a similar RPC framework developed by Facebook/Apache. It uses its own interface definitions language and provides code generation for various programming languages.

FeaturegPRCApache Thrift
PerformanceFastA little faster
ProtocolHTTP/2Any*
StreamingNative supportVia addons
CommunityLargerSmaller

* Apache Thrift provides an abstraction over network IO and can run on different transport implementations, such as TCP, HTTP, etc.

GraphQL

GraphQL is more suitable for public APIs that are exposed to consumers directly, where the clients need more control over the data that consume from the server. However, it adds an overhead that might be redundant for private services communication.

Use Cases

The gRPC framework is oriented toward microservices communication. Therefore, it can be used for most synchronous message-exchanging scenarios.

However, it’s not recommended for asynchronous communication, where message persistence is required (message brokers like Kafka or RabbitMQ can do that job). It’s also not the best choice for the publicly exposed API interfaces, which is better to provide using well-known RESTful or GraphQL styles.

The most appropriate use cases:

  • Real-time communication between internal microservices
  • Multi-programming language environments
  • Systems with limited resources and latency network
  • When efficient and fast communication is the priority
  • Device-to-Device and Device-to-Cloud communication for embedded systems

Protocol Buffers

By default, gRPC uses Protocol Buffers as a messaging mechanism that serializes underlying data according to the declared structure.

Protocol Buffers (Protobuf) is Google’s serialization/deserialization protocol that provides definition interfaces and client’s code generation. It comes with its own Interface Definition Language (IDL), which allows describing the entities and service methods.

The Protobuf compiler, protoc, generates code that loads .proto files into memory in runtime and uses them to work with binary messages. That flow works much more efficiently than text format messages (JSON, HTML) because encoded messages are smaller.

Messages

Protocol buffer data is structured as messages containing valuable information described in name/value pairs called fields. The message definitions are defined in proto files that have a .proto file extension.

message Person {
  string name = 1;
  int32 id = 2;
  bool has_ponycopter = 3;
} 

Services

The other piece is a protocol buffer service that describes available methods (operations) and their input/output types. In other words, service is a collection of remote methods exposed to a client.

Protocol buffer service methods can have only one input parameter and one output. If a method needs to apply a few parameters, they should be grouped within a single message.

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

Data Types

Scalar Value Types:

TypeKeywordsDefault
Doubledouble, float0
Integerint32, int64, uint32, uint64, sint32, sint64, fixed32, fixed64, sfixed32, sfixed640
Booleanboolfalse
Stringstring, bytes""

Enumerations

Enums present a value from a defined list of declared options. Every enum declaration must have a constant that maps to 0 (used as the default value) and be specified as the first element.

The reserved keyword is used for some values/constants that are removed and shouldn’t be reused in the future to preserve backward compatibility.

enum Status {
  option allow_alias = true; // optional to enable aliases

  reserved 2, 15, 9 to 11, 40 to max; // restricts values usage
  reserved "FOO", "BAR"; // restricts keys usage

  OK = 0;
  ERROR = 1;
  BAD_REQUEST = 1; // the alias for the 'ERROR' constant
}

message ServerResponse {
  Status status = 1;
}

Nested Types

That’s possible to define a nested message type to keep the definitions close to their parent. However, the nested types are not fully encapsulated and can be used outside, referring to their parent (<Parent>.<Type>).

message Response {
  message ResponseData {
    string title = 1;
    int32 price = 1;
  }

  repeated ResponseData data = 1;
}

message SearchResponse {
  Response.ResponseData item = 1;
}

Special Types

The complete reference is available at the Protocol Buffers Well-Known Types.

import "google/protobuf/any.proto";
import "google/protobuf/timestamp.proto";

message Response {
  map<string, int32> statusCodes = 2; // key-value pairs object
  repeated string tags = 3; // array of strings

  Any status = 1; // allows any type
  Timestamp requestedAt = 4; // date format
}

Versions

Currently, there are two Protocol Buffers syntax versions: proto 2 and proto 3.

The proto2 is the initially published version that is still fully supported.

The proto3 is available for all the programming languages, supported by gRPC, and has a nicer declaration syntax compared to its previous version. It is the current version that is recommended for new projects.

The version is specified in the .proto file using the syntax keyword:

syntax = "proto3";

Code Generation

Protocol Buffers provide code generation for the targeting programming language. Its compiler (protoc) can generate data access classes in a selected language using existing .proto definitions. These classes will have properties accessors (get/set) and functionality that allows to serialize/deserialize data (because it’s transferred in binary format).

The general process of gRPC integrations consists of the following steps:

  1. Create a service interface definition using .proto files
  2. Generate server-side and client-side in appropriate programming languages
  3. Implement a server application:
    • Implement the logic of the declared service methods
    • Run a gRPC server that binds the implemented service
  4. Implement a client application:
    • Connect to the remote gRPC server
    • Invoke the remote methods using client-side generated code

Protocol Buffers Concepts

Node.js

Although @grpc/grpc-js library uses dynamic code generation (.proto files are loaded at runtime), it’s possible to use an appropriate plugin to pre-generate the JavaScript stubs definitions.

$ npm install --save grpc-tools google-protobuf
$ npx grpc_tools_node_protoc --js_out=import_style=commonjs,binary:. --grpc_out=grpc_js:. greeter.proto

This command will generate the following files that will be used for the gRPC server and client:

  • greeter_grpc_pb.js - server stubs
  • greeter_pb.js - message builders

If you want to use these files in the ES Modules project, change their file extension to .cjs.

Python

Protocol Buffers compiler (protoc) allows generating a gRPC-base code using a plugin. For example, the python code can be created by the following commands:

$ pip3 install grpcio grpcio-tools # install dependencies
$ python3 -m grpc_tools.protoc -I. --python_out=. --pyi_out=. --grpc_python_out=. ./greeter.proto

Assuming we have a greeter.proto protobuf definition file, it will create the following files:

  • greeter_pb2_grpc.py
  • greeter_pb2.py
  • greeter_pb2.pyi

For each Protocol Buffer service definition, the primary elements are created (greeter_pb2_grpc.py file):

  • Stub (GreeterStub) - a class the clients use to connect to the created gRPC server.
  • Servicer (GreeterServicer) - a base class for the service methods implementation. Its methods should be overwritten in the child class to provide the gRPC service logic.
  • Registration Function (add_GreeterServicer_to_server) - a function used to register an implemented servicer with a grpc.Server object.

The greeter_pb2.pyi file will contain builder classes for each message in the Protobuf definition. These classes should be used to work with the specified gRPC entities.

Streaming

The stream is a sequence of multiple requests/responses. Streams are a core feature of the gRPC framework that allows for performing many processes in a single request. That’s possible by the multiplexing capability of the HTTP/2 protocol.

Server Streaming

In a server-streaming RPC mode, the client sends a single request, and the server returns a stream of messages as the response. After sending all the server responses, the server marks the end of the stream by sending the server’s status details as trailing metadata to the client. During the streaming process, the client reads messages from the stream until no messages are left.

service Location {
  rpc Track(Empty) returns (stream Point);
}

The server has access to a writable stream object. Once all the messages are sent, the stream must be ended.

function Track(stream) {
  // start streaming data from the server
  stream.write({ latitude: 22.4395, longitude: 34.8312 });
  stream.write({ latitude: 22.5103, longitude: 34.8082 });
  stream.write({ latitude: 22.5492, longitude: 34.7911 });

  stream.end(); // complete the streaming
}

The client is also accessing the stream and reading the messages from there:

const trackStream = client.Track(); // initiate the procedure call
trackStream.on('data', point => {}); // read messages from the stream
trackStream.on('end', () => console.log('Stream has ended')); // completion

Client Streaming

In a client-streaming RPC mode, the client sends a sequence of messages to the server using a provided stream. The server returns a single response with the result of processing to the client. The response might be returned anytime, even without waiting for all the client messages to be received.

The server can stop processing the client’s stream and cancel it, in which case the client will be notified and stop producing new messages.

The message sequence is passed in a single independent RPC call.

service Location {
  rpc Share(stream Point) returns (Empty);
}

The server has access to a readable stream object with the client’s messages:

function Share(stream, callback) {
  stream.on('data', point => {}); // process client's message

  stream.on('end', () => {
    callback(null, result);  // the result response
  });
}

The client is accessing a writable stream for the message publishing:

const shareStream = client.Share((error, response) => {
  // server's response once all client's messages are sent
});

// stream client's messages
stream.write({ latitude: 22.4395, longitude: 34.8312 });
stream.write({ latitude: 22.5103, longitude: 34.8082 });
stream.write({ latitude: 22.5492, longitude: 34.7911 });

// notify that all the messages are sent
shareStream.end();

Bidirectional Streaming

In a bidirectional RPC mode, both the client and server exchange a sequence of messages independently. On both ends, read-write streams are available to send and receive messages. The order of messages in each direction is preserved.

The bidirectional RPC procedure has to be initiated by the client call, but after that, the communication is performed based on the business logic, where both client and server can produce their messages in any order.

service Location {
  rpc Tune(stream Point) returns (stream TuneOptions);
}

The server has access to the read-write stream, which means it can send messages to the client and read the client’s messages at the same time:

function Tune(stream) {
  stream.on('data', point => { // process client's message
    // respond to a client immediately ("ping-pong")
    stream.write({ tune: Math.random() }); 
  }); 

  stream.on('end', () => { stream.end() }); // once no more client's messages
}

The client is working the same read-write stream:

let tune = 0;
const tuneStream = client.Tune();

// listen for server's messages
tuneStream.on('data', options => { tune = options.tune; });

// stream client's messages
tuneStream.write({ latitude: tune + 1, longitude: tune + 1 });
tuneStream.write({ latitude: tune + 1, longitude: tune + 1 });
tuneStream.write({ latitude: tune + 1, longitude: tune + 1 });

tuneStream.end(); // complete the client's stream

Examples

Resources

Top