How we use gRPC to build a client/server system in Go

Julien Andrieux
Oct 9, 2017 · 19 min read

This post is a technical presentation on how we use gRPC (and Protobuf) to build a robust client/server system.

I won’t go into the details of why we chose gRPC as the main communication protocol between our client and server, a lot of great posts already cover the subject (like this one, this one or this one). But just to get the big picture: we are building a client-server system, using Go, that needs to be fast, reliable, and that scales (thus we chose gRPC). We wanted the communication between the client and the server to be as small as possible, as secure as possible and fully compliant with both ends (thus we chose Protobuf).

We also wanted to expose another interface on the server side, just in case we add a client that has no compatible gRPC implementation: a traditional REST interface. And we wanted that at (almost) no cost.

Context

We will start building a very simple client/server system, in Go, that will just exchange dummy messages. Once both communicate and understand each other, we’ll add extra features, such as TLS support, authentication, and a REST API.

The rest of this articles assumes you understand basic Go programming. It also assumes that you have protobuf package installed, and the protoccommand available (once again, many posts cover that topic, and there’s the official documentation).
You will also need to install Go dependencies, such as the go implementation for protobuf, and grpc-gateway.

All the code shown in this post is available at https://gitlab.com/pantomath-io/demo-grpc. So feel free to get the repository, and use the tags to navigate in it. The repository should be placed in the src folder of your $GOPATH:

Defining the protocol

git tag: init-protobuf-definition

Image for post
Image for post
Photo by Mark Rasmuson on Unsplash

First of all, you need to define the protocol, i.e. to define what can be said between client and server, and how. This is where Protobuf comes into play. It allows you to define two things: Services and Messages. A service is a collection of actions the server can perform at the client’s request, a message is the content of this request. To simplify, you can say that service defines actions, while message defines objects.

Write the following in api/api.proto:

So, you defined 2 things: a service called Ping that exposes a function called SayHello with an incoming PingMessage and returns a PingMessage ; and a message called PingMessage that consists in a single field called greeting which is a string.
You also specified that you are using the proto3 syntax, as opposed to proto2 (see documentation).

This file is not usable like this: it needs to get compiled. Compiling the proto file means generating code for your chosen language, that your application will actually call.
In a shell, cd to the root directory of your project, and run the following command:

This command generates the file api/api.pb.go, a Go source file that implements the gRPC code your application will use. You can look at it, but you shouldn’t change it (as it will be overwritten every time you run protoc).

You also need to define the function called by the service Ping, so create a file named api/handler.go:

  • the Server struct is just an abstraction of the server. It allows to “attach” some resources to your server, making them available during the RPC calls;
  • the SayHello function is the one defined in the Protobuf file, as the rpc call for the Ping service. If you don’t define it, you won’t be able to create the gRPC server;
  • SayHello takes a PingMessage as parameter, and returns a PingMessage. The PingMessage struct is defined in the api.pb.go file auto-generated from the api.proto definition. The function also has a Context parameter (see further presentation in the official blog post). You’ll see later what use you can do of the Context. On the other side, it also returns an error, in case something bad happens.

Creating the simplest server

git tag: init-server

Image for post
Image for post
Photo by Nathan Dumlao on Unsplash

Now you have a protocol in place, you can create a simple server that implements the service and understands the message. Take your favorite editor and create the file server/main.go:

Let me break down to code to make it clearer:

  • note that you import the api package, so that the Protobuf service handlers and theServer struct are available;
  • the main function starts by creating a TCP listener on the port you want to bind your gRPC server to;
  • then the rest is pretty straight forward: you create an instance of your Server, create an instance of a gRPC server, register the service, and start the gRPC server.

You can compile your code to get a server binary:

Creating the simplest client

git tag: init-client

Image for post
Image for post
Photo by Clem Onojeghuo on Unsplash

The client also imports the api package, so that the message and the service are available. So create the file client/main.go:

Once again, the break down is pretty straight forward:

  • the main function instantiates a client connection, on the TCP port the server is bound to;
  • note the defer call to properly close the connection when the function returns;
  • the c variable is a client for the the Ping service, that calls the SayHello function, passing a PingMessage to it.

You can compile your code to get a client binary:

Make them talk

You’ve just built a client and a server, so fire them in two terminals to test them:

Tool to ease your life

git tag: init-makefile

Now the API, the client and the server are working, you may prefer to have a Makefile to compile the code, clean your folder, manage dependencies, etc.

So create this Makefile at the root of the project folder. Explaining this file is beyond the scope of this post, and it mostly uses compile command you already spawned previously.

To use the Makefile , try calling the following:

Secure the communication

git tag: add-ssl

Image for post
Image for post
Photo by Nathaniel Tetteh on Unsplash

The client and the servers talk to each other, over HTTP/2 (transport layer on gRPC). The messages are binary data(thanks to Protobuf), but the communication is in plaintext. Fortunately, gRPC has SSL/TLS integration, that can be used to authenticate the server (from the client’s perspective), and to encrypt message exchanges.

You don’t need to change anything to the protocol: it remains the same. The changes take place in the gRPC object creation, on both client and server side. Note that if you change only one side, the connection won’t work.

Before you change anything in the code, you need to create a self-signed SSL certificate. The purpose of this post is not to explain how to do that, but the official OpenSSL documentation (genrsa, req, x509) can answer your question about it (DigitalOcean also has a nice and complete tutorial about it). Meanwhile, you can just use the files provided in the cert folder. The following commands have been used to generate the files:

You can proceed and update the server definition to use the certificate and the key:

So what changed?

  • you created a credentials object (called creds) from your certificate and key files;
  • you created a grpc.ServerOption array and placed your credentials object in it;
  • when creating the grpc server, you provided the constructor with you array of grpc.ServerOption;
  • you must have noticed that you need to precisely specify the IP you bind your server to, so that the IP matches the FQDN used in the certificate.

Note that grpc.NewServer() is a variadic function, so you can pass it any number of trailing arguments. You created an array of options so that we can add other options later on.

If you compile your server now, and use the client you already have, the connection won’t work, and both sides will throw an error.

  • the server report the client is not handshaking with TLS:
  • the client has its connection closed before it can do anything:

You need to use the exact same certificate file on the client side. So edit the client/main.go file:

The changes on the client side are pretty much the same as on the server:

  • you created a credentials object with the certificate file. Note that the client do not use the certificate key, the key is private to the server;
  • you added an option to the grpc.Dial() function, using your credentials object. Note that the grpc.Dial() function is also a variadic function, so it accepts any number of options;
  • same server note applies for the client: you need to use the same FQDN to connect to the server as the one used in the certificate, or the transport authentication handshake will fail.

Both sides use credentials, so they should be able to talk just as before, but in an encrypted way. You can compile the code:

And run both sides in separate terminals:

Identify the client

git tag: add-auth

Image for post
Image for post
Photo by chuttersnap on Unsplash

Another interesting feature of the gRPC server is the ability to intercept a request from the client. The client can inject information on the transport layer. You can use that feature to identify your client, because the SSL implementation authenticates the server (via the certificate), but not the client (all your clients are using the same certificate).

So you’ll update the client side to inject metadata on every call (like a login and password), and the server side to check these credentials for every incoming call.

On the client side, you just need to specify a DialOption on your grpc.Dial() call. But that DialOptionhas some constraints. Edit your client/main.go file:

  • You define a struct to hold the collection on fields you want to inject in your rcp calls. In our case, just a login and password, but you can imagine any fields you want;
  • The auth variable holds the values you’ll be using;
  • You use grpc.WithPerRPCCredentials() function to create a DialOption object to the grpc.Dial() function;
  • Note that the grpc.WithPerRPCCredentials() function takes an interface as parameter, so your Authentication structure should comply to that interface. From the documentation, you know you should implement 2 methods on your structure: GetRequestMetadata and RequireTransportSecurity.
  • So you define GetRequestMetadata function that just returns a map of your Authentication structure;
  • And finally, you define RequireTransportSecurity function, that tells your grpc client if it should inject metadata at the transport level. In our current case, it always returns true, but you could have it return the value of a configuration boolean, for instance.

The client is up to push extra data during its calls to the server, but the server does not care, right now. So you need to tell him to check these metadata. Open server/main.go and update it:

Once again, let me break down this for you:

  • you add a new grpc.ServerOption to the array you created before (see why it’s an array, now?): grpc.UnaryInterceptor. And you pass a reference to a function to that function, so it knows who to call. The rest of the main code does not change;
  • you have to define the unaryInterceptor function, considering it gets a bunch of parameters:
  1. a context.Context object, containing your data, and that will exist during all the lifetime of the request;
  2. an interface{} which is the inbound parameter of the RPC call;
  3. a UnaryServerInfo struct which contains a bunch of information about the call (such as the Server abstraction object, and the method call by the client);
  4. a UnaryHandler struct which is the handler invoked by UnaryServerInterceptor to complete the normal execution of a unary RPC (i.e. an handler to what happens when the UnaryInterceptor returns).
  • the unaryInterceptor function makes sure the grpc.UnaryServerInfo has the right server abstraction, and call the authentication function, authenticateClient;
  • you define the authenticateClient function with your authentication logic — very very simple in this example. Note that it receives the context.Context as parameter, and extract the metadata from it. It checks the user, and returns its ID (in the form of a string, with a hypothetical error.
  • if the unaryInterceptor gets no error from the authenticateClient function, it pushes the clientID in the context.Context object, so that the rest of the execution chain can use it (remember the handler gets the context.Context object as parameter?);
  • Note that you created your type and const to reference the clientID in the context.Context map. This is just an handy way to avoid naming conflict and to allow constant reference.

You can compile the code:

And run both sides in separate terminals:

Obviously, your authentication logic will probably be smarter, comparing credentials against a database. The easy part of it is: your authentication function gets your abstraction of a Server, and this structure can hold your database handler.

Open to REST

git tag: add-rest

Image for post
Image for post
Photo by Rio Hodges on Unsplash

One last thing: you have a pretty neat server, client and protocol; serialized, encrypted and authenticated. But there is a important limit: your client needs to be gRPC compliant, that is be in the list of supported platforms. To avoid that limit, we can open the server to a REST gateway, allowing REST clients to perform requests it. Luckily, there is a gRPC protoc plugin to generate a reverse-proxy server which translates a RESTful JSON API into gRPC. We can use a few line of pure Go code to serve that reverse-proxy.

So edit your api/api.proto file to add some extra information:

The annotations.proto import allows protoc to understand the option set later in the file. And the option defines that method and the path to the endpoint.

Update the Makefile to add a target for this new Protobuf compilation:

Generate the Go code for the gateway (the file api/api.pb.gw.go will be generated — just as api/api.pb.go, don’t edit it, it will be updated by compilation):

The change on the server side is more important. The grpc.Serve() function is a blocking function, that returns only on error (or can get killed by a signal). As we need to start another server (the REST interface), we need this call to be non-blocking. Fortunately, we have goroutines just for that. And there is a trick on the authentication. As the REST gateway is just a reverse-proxy, it acts as a client from the gRPC perspective. Thus it needs to use a WithPerRPCCredentials option when dialing the server.

Head to your server/main.go file:

So what happened?

  • you moved all the code for the gRPC server creation in a goroutine with a dedicated function (startGRPCServer), so it does not block the main;
  • you create a new goroutine with a dedicated function (startRESTServer) where you create an HTTP/1.1 server;
  • in startRESTServer where you create the REST gateway, you start by getting the context.Context background object (i.e. the root of the context tree). Then, you create a request multiplexer object, mux, with an option: runtime.WithIncomingHeaderMatcher. This option takes a function reference as parameter, credMatch, and is called for every HTTP header from the incoming request. The function evaluates whether or not the HTTP header should be passed to the gRPC context;
  • you defined the credMatch function to match the credentials header, allowing them to be metadata in the gRPC context. This is how you have your authentication working, because the reverse-proxy uses the HTTP headers it receives when it connects to the gRPC server;
  • you also create a credentials.NewClientTLSFromFile, to be used as a grpc.DialOption, just like you did in the client side;
  • you register your api endpoint, i.e. you make the link between your multiplexer, you gRPC server, using the context and the gRPC options;
  • and finally, you start an HTTP/1.1 server, and wait for incoming connections;
  • aside to your goroutine, you use a blocking select call, so that your program does not end right away.

Now build the whole project, so you can test the REST interface:

And run both sides in separate terminals:

One last swag…

git tag: add-swagger

Image for post
Image for post
Photo by Lorenzo Castagnone on Unsplash

The REST gateway is cool, but it would be even cooler to generate documentation from it, right?

You can do that for free, using a protoc plugin to generate a swagger json file:

This will generate a api/api.swagger.json file. As all generated code from Protobuf compilation, you should not edit it, but you can use it, and it can update it when you change your definition file.

You can add the compilation command in the Makefile.

Conclusion

You have a fully functional gRPC client and server, with SSL encryption & authentication, client identification, and a REST gateway (with its swagger file). Where to go from here?

You can push a little on the REST gateway, to make it HTTPS instead of HTTP. You can obviously add more complex data structure on your Protobuf, alongside with more service. You can benefit from HTTP/2 features, such as streaming, either from client to server, or from server to client, or bidirectional (but that’s only for gRPC, not for the REST, based on HTTP/1.1).

Many thanks to Charles Francoise who co-wrote this paper and https://gitlab.com/pantomath-io/demo-grpc.

We are currently working on Pantomath. Pantomath is a modern, open-source monitoring solution, built for performance, that bridges the gaps across all levels of your company. The well-being of your infrastructure is everyone’s business. Keep up with the project : goo.gl/tcxtXq

Pantomath is a cutting edge monitoring solution

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store