In this article I’m going to describe how to build simplest gRPC API. Building such API is consists of several steps:
- First of all, we have to define our API interface with protocol buffers or protobuf.
- Then we have to generate Golang server and client stubs with
protoc
command. - 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 usego
plugin with nameprotoc-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 tocalc.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 callinggrpc.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.