gRPC API: Easy start

Building simplest gRPC service.

In this article I’m going to describe how to build simplest gRPC API. Building such API is consists of several steps:

  1. First of all, we have to define our API interface with protocol buffers or protobuf.
  2. Then we have to generate Golang server and client stubs with protoc command.
  3. And finally we need to implement our API.

What our service will be doing is a calculations. At the moment it has just one method Add which takes A and B and returns C as an addition result.

Project can be found on github.

Protocol buffers or protobuf

Here is calc.proto file with protobuf definition of the service:

syntax = "proto3";
option go_package = "pb";

message Request {
  int32 a = 1;
  int32 b = 2;
}

message Response {
  int64 c = 1;
}

service Calc {
  rpc Add (Request) returns (Response);
}

This definition contains Calc service with one Add method which takes message Request with a and b fields and returns Response with c field. Numbers in message definitions are called tags and used by gRPC server and client in encoding/decoding messages procedures. In fact, it’s a field position inside decoded message.

This definition is language independent, that means using this file we can generate server and client stubs for any language supported by protobuf compiler protoc.

protoc supports number of languages out-of-the-box, but for Golang we have to use plugin. In our case we need plugin for Golang called protoc-gen-go. Plugins are just a binaries which should be located in any directory available in $PATH. Plugin name has a format protoc-gen-<suffix>, where:

  • protoc-gen- is a prefix.
  • <suffix> - plugin short name without prefix.

Server and client stubs

Ok, let’s generate it:

protoc --go_out=plugins=grpc:. ./calc.proto

Here:

  • protoc is a protobuf compiler command.
  • go_out is a parameter which says to use go plugin with name protoc-gen-go. In general, this parameter has a format <suffix>_out.
  • plugins=grpc is a parameter for the plugin. It says to the plugin add an interface definition for our service into auto-generated file.
  • :. part after semicolon is a output path . (current directory) for auto-generated files related to calc.proto file.
  • ./calc.proto is a path to the service protobuf definition.

The command creates calc.pb.go file in the same directory with server and client interfaces, structures for request and response and some other functions for encoding/decoding messages.

Most interesting parts for us from the new file are Request and Response structures and CalcServer/CalcClient interfaces, you can see below:

type Request struct {
	A int32 `protobuf:"varint,1,opt,name=a,proto3" json:"a,omitempty"`
	B int32 `protobuf:"varint,2,opt,name=b,proto3" json:"b,omitempty"`
}

type Response struct {
	C int64 `protobuf:"varint,3,opt,name=c,proto3" json:"c,omitempty"`
}

type CalcServer interface {
	Add(context.Context, *Request) (*Response, error)
}

type CalcClient interface {
	Add(ctx context.Context, in *Request, opts ...grpc.CallOption) (*Response, error)
}

gRPC server implementation

We have protobuf service definition and auto-generated code by this definition. Now, we will be implementing gRPC server to be able to accept new connections and return responses.

type server struct{}

func (s *server) Add(ctx context.Context, req *pb.Request) (*pb.Response, error) {
	fmt.Printf("got request: A = %d, B = %d\n", req.A, req.B)
	return &pb.Response{
		C: req.A + req.B,
	}, nil
}

func main() {
	s := grpc.NewServer()
	pb.RegisterCalcServer(s, &server{})

	lis, err := net.Listen("tcp", ":5001")
	if err != nil {
		return
	}

	s.Serve(lis)
}

server structure implements CalcServer interface from calc.pb.go file, which represents gRPC server methods. In main function we:

  • initialize empty gRPC server by calling grpc.NewServer, it has no methods and it doesn’t listen for any connections yet.
  • register empty gRPC server with our implementation, i.e. we attach methods to the server.
  • create a TCP listener for accepting incoming connections.
  • start serve connections with created listener.

Build and start the server:

% go build && ./server

Now we need a client and here we are:

func main() {
	cc, err := grpc.Dial("127.0.0.1:5001", grpc.WithInsecure())
	if err != nil {
		log.Fatal(err)
	}
	defer cc.Close()

	client := pb.NewCalcClient(cc)
	resp, err := client.Add(context.Background(), &pb.Request{A: 2, B: 2})
	if err != nil {
		log.Fatal(err)
	}

	fmt.Printf("response is: %d\n", resp.C)
}

In the client code we:

  • initialize gRPC client connection cc by calling grpc.Dial.
  • grpc.WithInsecure here says grpc client to disable transport security.
  • initialize our CalcClient.
  • call Add method passing there request with some values.

Now, let’s build and run the client in the separate terminal session:

% go build && ./client
response is: 4

while in server terminal we’ll see request info like this:

% ./server
got request: A = 2, B = 2

That’s pretty enough to the simplest gRPC API.