Getting started with gRPC

gRPC is a modern, open source remote procedure call (RPC) framework that can run anywhere. It enables client and server applications to communicate transparently, and makes it easier to build connected systems. Lifted from the FAQ page directly.

In this post I’ll go over how to set it up in a fresh Ubuntu 19.04 install. I’ll then go over creating an initial gRPC server in Go and a client in Python.

If installing in another version of Ubuntu, or any other Linux distribution, then your mileage may vary.

Install

First we’ll install Go, Python, and Pip.

$ sudo apt install golang-go python3 python3-pip

We need both gRPC and protobuf for Go.

$ sudo apt install protobuf-compiler golang-goprotobuf-dev
$ go get -u google.golang.org/grpc

And the same for Python.

$ pip3 install grpcio grpcio-tools

Protobuf API

The service I’ll be building will take an IP address as a request, and the response will show if the address is valid, what range it falls in, and whether the server was able to ping the address. This proto file will be called ip.proto.

syntax = "proto3";

package ipinfo;

service ip_info {
   rpc check_ip (ip_request) returns (ip_response) {}
}

message ip_request {
    string address = 1;
}

message ip_response {
    bool valid = 1;
    string range = 2;
    bool ping_response = 3;
}

The protobuf definition has three main parts. First is the header itself showing what syntax the file will be using, and what package this file belongs to.

syntax = "proto3";

package ipinfo;

Second is the service itself that will run on the server. Here I’ve only defined a single RPC, but a server can have as many RPC defined as you need.

service ip_info {
   rpc check_ip (ip_request) returns (ip_response) {}
}

The third part is the messages themselves. The RPC called check_ip will take in an ip_request and return an ip_response. These latter two are messages defined. Each message has items defined inside of them, much like a struct.

message ip_request {
    string address = 1;
}

message ip_response {
    bool valid = 1;
    string range = 2;
    bool ping = 3;
}

Convert

Now that we have the .proto file, we need to convert that into a library that we can import into our app. The beauty here is that the library code gives you all the functions you need to interact with the service. For these examples I’m currently in the same directory as the ip.proto file.

python3 -m grpc_tools.protoc -I. --python_out=. --grpc_python_out=. ip.proto
protoc -I . ip.proto --go_out=plugins=grpc:.

Three new files will be created. Two Python files and a Go file. These library files will be imported into our app to give us the functionality required.

Server boilerplate

I’ll start with the boilerplate server Go code for this service. I won’t implement the CheckIp function yet, but it’s required in order for the server to compile.

package main

import (
	"context"
	"log"
	"net"

	pb "github.com/mellowdrifter/ipinfo/proto"
	"google.golang.org/grpc"
)

type server struct{}

func (s *server) CheckIp(ctx context.Context, r *pb.IpRequest) (*pb.IpResponse, error) {
	return &pb.IpResponse{}, nil
}

func main() {
	// set up gRPC server
	log.Printf("Listening on port %d\n", 9999)
	lis, err := net.Listen("tcp", ":9999")
	if err != nil {
		log.Fatalf("Failed to bind: %v", err)
	}
	grpcServer := grpc.NewServer()
	pb.RegisterIpInfoServer(grpcServer, &server{})

	grpcServer.Serve(lis)
}

I’ve imported the created proto file, created the required function, and set up the server to listen to request on localhost port 9999.

RPC implementation

gRPC will pass a struct to the function, defined as the message in the proto file. The generated code from protoc will also give us the functions required to dig into that struct safely. I’ll get the function to do the required work and fill in the struct response back

func (s *server) CheckIp(ctx context.Context, r *pb.IpRequest) (*pb.IpResponse, error) {
	log.Printf("Received request for %s\n", r.GetAddress())
	response := &pb.IpResponse{}

	// Is the IP valid?
	valid := net.ParseIP(r.GetAddress())
	if valid == nil {
		return response, nil
	}
	response.Valid = true

	// What kind of IP is this?
	switch {
	case valid.IsLoopback():
		response.Range = "loopback"
	case valid.IsMulticast():
		response.Range = "multicast"
	case valid.IsGlobalUnicast():
		response.Range = "global unicast"
	default:
		response.Range = "unknown"
	}

	// Can we ping the address?
	pinger, err := ping.NewPinger(fmt.Sprintf("%s", valid))
	if err != nil {
		return response, err
	}
	pinger.SetPrivileged(true)
	pinger.Count = 5
	pinger.Timeout = 1000000000
	pinger.Run()
	if pinger.Statistics().PacketsRecv > 0 {
		response.PingResponse = true
	}
	return response, nil
}

Client

The client will take an argument from the CLI and send that to the server. It’ll then print out the results it gets back.

#!/usr/bin/env python3

import ip_pb2 as pb
import ip_pb2_grpc
import grpc
import sys

server = "127.0.0.1"
port = "9999"

# Set up GRPC server details
grpcserver = "%s:%s" % (server, port)
channel = grpc.insecure_channel(grpcserver)
stub = ip_pb2_grpc.ip_infoStub(channel)

# Get info
response = stub.check_ip(pb.ip_request(address=sys.argv[2]))

if response.valid == True:
    output = "{} is a valid IP. It falls in the {} range.".format(sys.argv[1], response.range)
    if response.ping_response == True:
        output += " I was able to ping the address."
    else:
        output += " I was unable to ping the address."
else:
    output = "{} is not a valid IP address".format(sys.argv[1])

print(output)

Call the RPC

I’ll now run the server.

$ sudo go run ip.go
2019/05/03 22:52:08 Listening on port 9999

I’ll run a couple of tests from the client

$ ./ip.py 8.8.8.8
8.8.8.8 is a valid IP. It falls in the global unicast range. I was able to ping the address.
$ ./ip.py 224.0.0.1
224.0.0.1 is a valid IP. It falls in the multicast range. I was able to ping the address.
$ ./ip.py 11.0.0.1
11.0.0.1 is a valid IP. It falls in the global unicast range. I was unable to ping the address.
$ ./ip.py 11.0.0.300
11.0.0.300 is not a valid IP address

Outputs on the server

$ sudo go run ip.go
2019/05/03 23:02:02 Listening on port 9999
2019/05/03 23:02:05 Received request for 8.8.8.8
2019/05/03 23:02:11 Received request for 224.0.0.1
2019/05/03 23:02:17 Received request for 11.0.0.1
2019/05/03 23:02:25 Received request for 11.0.0.300

end