Aug 17, 2024

Notes on RPC(Remote Procedure Call)

RPC stands for Remote Procedure Call. It's a communication paradigm== used in distributed systems to allow a program running on one computer to execute a procedure (function) on a different computer like a local procedure call.

Image credit

Key components of RPC

  1. Client: The client is the program that initiates the RPC call. It's the one that knows what procedure to call and what arguments to pass.

  2. Client Stub: The client stub is a piece of code that sits on the client side. It's responsible for marshalling the arguments, sending the request over the network, and receiving the response.

Client stubs can use various formats for marshalling the arguments. This includes formats like JSON, XML, or binary.

  1. Server: The server is the program that provides the service being called. It's the one that actually executes the procedure.

  2. Server Stub: The server stub is a piece of code that sits on the server side. It's responsible for receiving the request, unmarshalling the arguments, executing the procedure, and sending the response back to the client.

  3. Network Protocol: The network protocol is the set of rules and conventions that govern how the client and server communicate with each other. This can include protocols like TCP/IP, HTTP, or custom protocols.

  4. IDL (Interface Definition Language): IDL is a language used to define the interface between the client and server. It specifies the procedures that can be called, the arguments they take, and the return values.

Here's a quick illustration of how these components interact:

Why do we need RPC, when we already had HTTP (or REST) ?

  1. REST over HTTP carries extra metadata (has headers, uses JSON/XML request/responses ) in nature requiring higher network bandwidth and thereby increasing latency.

  2. HTTP doesnt support retries, timeouts etc. these have to be built by developers. RPC frameworks like gRPC provide these features out of the box.

  3. RPC is language and platform agnostic. The client and server stubs are generated from the IDL.

Implmenting simple RPC with gRPC

gRPC is a framework developed by Google. gRPC uses Protocol Buffers (protobuf) as its interface definition language (IDL) and HTTP/2 as its transport protocol.

Lets implment a simple RPC service using gRPC step by step in js.

  1. Define the Service Definition

Create a file named hello.proto in your project directory:

syntax = "proto3";

package helloworld;

// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greeting.
message HelloReply {
string message = 1;
}
syntax = "proto3";

package helloworld;

// The greeting service definition.
service Greeter {
// Sends a greeting
rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
string name = 1;
}

// The response message containing the greeting.
message HelloReply {
string message = 1;
}
  1. Generate gRPC JavaScript Code
grpc_tools_node_protoc --js_out=import_style=commonjs:. --grpc_out=grpc_js:. hello.proto
grpc_tools_node_protoc --js_out=import_style=commonjs:. --grpc_out=grpc_js:. hello.proto

This command uses the grpc_tools_node_protoc tool to generate two JavaScript files:

  • hello_pb.js: Contains the JavaScript code for the HelloRequest and HelloReply message types.
  • hello_grpc_pb.js: Contains the JavaScript code for the Greeter service definition and the client/server stubs.
  1. Implement the gRPC Server (hello_server.js):

Create a file named hello_server.js:


const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const PROTO_PATH = __dirname + '/hello.proto';

const packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

function sayHello(call, callback) {
callback(null, {message: 'Hello, ' + call.request.name + '!'});
}

function main() {
const server = new grpc.Server();
server.addService(hello_proto.Greeter.service, { SayHello: sayHello });
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
server.start();
console.log('gRPC server started on port 50051');
});
}

main();


const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const PROTO_PATH = __dirname + '/hello.proto';

const packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

function sayHello(call, callback) {
callback(null, {message: 'Hello, ' + call.request.name + '!'});
}

function main() {
const server = new grpc.Server();
server.addService(hello_proto.Greeter.service, { SayHello: sayHello });
server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
server.start();
console.log('gRPC server started on port 50051');
});
}

main();

In this code:

  • We import the necessary grpc and proto-loader libraries.
  • We define the path to our .proto file and load its definition using protoLoader.loadSync.
  • We load the helloworld package from the loaded definition.
  • The sayHello function implements the logic for the SayHello RPC. It takes the call object (containing the request) and a callback function. We call the callback with null for the error and a response object containing the greeting.
  • The main function creates a new gRPC server, adds our Greeter service implementation, binds it to port 50051 using insecure credentials (for simplicity in this example), and starts the server.
  1. Implement the gRPC Client (hello_client.js)

Create a file named hello_client.js

const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const PROTO_PATH = __dirname + '/hello.proto';

const packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

function main() {
const client = new hello_proto.Greeter('localhost:50051', grpc.credentials.createInsecure());
const name = process.argv.length > 2 ? process.argv[2] : 'World';
const request = {name: name};

client.sayHello(request, (err, response) => {
if (err) {
console.error('Error:', err);
return;
}
console.log('Greeting:', response.message);
});
}

main();
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const PROTO_PATH = __dirname + '/hello.proto';

const packageDefinition = protoLoader.loadSync(
PROTO_PATH,
{
keepCase: true,
longs: String,
enums: String,
defaults: true,
oneofs: true
});
const hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

function main() {
const client = new hello_proto.Greeter('localhost:50051', grpc.credentials.createInsecure());
const name = process.argv.length > 2 ? process.argv[2] : 'World';
const request = {name: name};

client.sayHello(request, (err, response) => {
if (err) {
console.error('Error:', err);
return;
}
console.log('Greeting:', response.message);
});
}

main();

In this code:

  • We again import the necessary libraries and load the .proto definition.
  • In the main function, we create a gRPC client by instantiating hello_proto.Greeter with the server address (localhost:50051) and insecure credentials.
  • We get the name to greet from the command line arguments or default to "World".
  • We create a HelloRequest object with the provided name.
  • We call the sayHello method on the client stub, passing the request and a callback function. The callback handles the response (or any error) from the server and logs the greeting.
  1. Run the Server and Client
  • Run the server: node hello_server.js
  • Run the client: node hello_client.js Nathan

This example demonstrates a basic unary RPC in JavaScript using gRPC. The client sends a single request (HelloRequest) to the server, and the server sends back a single response (HelloReply). gRPC handles the underlying serialization (using Protocol Buffers), transport (using HTTP/2), and deserialization, allowing you to focus on defining your service and implementing the business logic.

RPC vs REST

CriteriaRPC (Remote Procedure Call)REST (Representational State Transfer)
DefinitionProtocol that allows executing procedures on remote systemsArchitectural style for networked applications using HTTP
Communication StyleAction-oriented (calls methods/functions)Resource-oriented (manipulates representations of resources)
ProtocolCan use various protocols (gRPC, JSON-RPC, XML-RPC)Typically uses HTTP/HTTPS
Data FormatDepends on implementation (Protocol Buffers, JSON, XML)Typically JSON or XML
PerformanceGenerally faster due to binary protocols (e.g., gRPC)Slightly slower due to text-based formats
When to Use
  • Internal microservices communication
  • High-performance requirements
  • Strongly typed systems
  • Real-time applications
  • Public APIs
  • Web applications
  • When caching is important
  • When discoverability is important
When Not to Use
  • Public-facing APIs (unless gRPC-Web)
  • When clients need simple HTTP access
  • When caching is important
  • Extremely performance-sensitive systems
  • When strongly typed contracts are needed
  • Real-time bidirectional streaming
Companies Using
  • Google (gRPC for internal services)
  • Netflix (for inter-service communication)
  • Uber (for microservices architecture)
  • Twitter (public API)
  • GitHub (public API)
  • Shopify (e-commerce APIs)
Use Cases
  • Internal service-to-service communication
  • Mobile app backends
  • IoT device communication
  • Public web APIs
  • Third-party integrations
  • Content delivery systems