Skip to content

A collection of gRPC and Go examples showcasing features of the framework

Notifications You must be signed in to change notification settings

vishal-uttamchandani/go-grpc

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

21 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Go and the gRPC Framework

If you are reading this, chances are you have some familiarity with Go and are looking to start working with gRPC. Or, maybe you are have been working with gRPC in a different language and are looking to start using it with Go. Either way, welcome!

This repository is a collection of code samples that showcases several features of the gRPC-go framework. Before we jump in too deep, let us start from the beginning with an overview of gRPC, exploring the nuts and bolts of its components.

Why gRPC

To explain gRPC, let us establish a scenario where we have a financial provider that wants to create a Currency Service that allows lookup and validation of currency info (i.e. name, code, country, and ISO number). The service is spec'd to have the following non-functional properties:

  • Be accessible from mobile front-ends (Java, Objective-C)
  • Accessible from backend other backends (Python, Java)
  • Accessible from Node.js backend to suppor JS front-end
  • Provides desktop access (#C) for a reporting tool

Your first reaction, to implement such service, may be to reach for JSON+HTTP (REST) to implement an HTTP-based API server. And, that would be a good choice as these technologies are mature and well-understood by the developer community. JSON was a response to the shortcomings of SOAP-era technologies and its soup of acronyms (let's not even mentioned the stuff SOAP replaced). Similarly, now gRPC is an evolution of this service-based approach that uses RPC to fill the technological gaps left by the REST including:

  • JSON offers weak data types
  • Lack of standardized machine-enforced interface contracts in JSON
  • JSON is a flexible but inefficient text-based encoding
  • Services are exposed as requests/responses of JSON docs causing a lost of semantics (HTTP only has GET,PUT,POST,DELETE, and PATCH)
  • Services code and clients are generally manually generated
  • Versioning, update, backward compatibility can be problems

If you are about to say SOAP, stop. While SOAP-based technologies introduced the machin-readable and enforceable contracts, it had its own shortmings:

  • Cumbersome contract definitions that were meant for machine, but often created by human
  • Encoding and decoding SOAP was so resource intensive, that it became its own market with vendors selling hardware to do it
  • XML over the wire is inefficient for low-bandwidth devices (mobile)
  • Tooling varied by platform and language

gRPC attempts to incorporate best practices from these technlogies by leveraging the efficiencies of protocol boffers and HTTP/2. It also introduce features not found in prior statck like bi-directional streaming allowing the creation of data intensive applications.

gRPC Overview

gRPC is a "high performance, open-source universal RPC framework". Specifically gRPC provides all the tools necessary to write services and clients, in a variety of languages, that can communicate by expressing remote services as transparent native methods on the client.

gRPC is designed to work efficiently with usage ranging from datacenter computing to small IoT devices. It continues where REST and SOAP left off and provides the following features:

  • Uses Protocol Buffers as a typed and efficient binary wire format
  • Machine-to-machine contracts are defined using a simple interface definition language (IDL)
  • Tools to generate code, from IDL, used to implement service methods and client code to invoke those methods remotely
  • Uses HTTP/2 which multiplexes long-lived connections for fast and efficient communication
  • Support for bi-directional streaming between client and servers
  • Extensive middleware API to control structural concerns such as security, logging, and service policy

Exploring Potocol Buffers

gRPC's efficiency is partly due to its use of protocol buffers (or protobuf). It is a language-neutral and platform-neutral technology to efficiently serialize data. Protocol buffers can be used independently of gRPC as a binary wire or storage format.

The first step to using protocol buffers is to define messages which are structures that consist of strongly-typed fields representing the data to be encoded. Protocol buffer supports fields of diverse tyes including numeric, string, boolean, enums, or other messages.

The following is a simple protobuf definition with two messages: Currency and CurrencyList:

syntax = "proto3";
package curproto;

message Currency {
    string code = 1; 
    string name = 2;
    int32 number = 3;
    string country = 4;
}

message CurrencyList {
    repeated Currency items = 1;
}

Protocol buffers file pb-examples/curproto/currency.proto

Messages are defined in a file with a .proto extension (convention) where they can be arranged as complex and nested data structures.

Compile the .proto file

The protobuf file by itself is not much use. The next step is to compile the file using the protocol buffers compiler (protoc) located at https://developers.google.com/protocol-buffers/. The compiler does the followings:

  • Create source code containing data structres based on the protobuf messages
  • Create code that can serialize and deserialize data into the generated structures

The compiler can generate code into several languages using a pluggable architecture. To generate Go code, download the Go generator for protoc using:

$> go get github.com/golang/protobuf/protoc-gen-go

To generate the Go code from the protobuf file, we can use the following command:

$ protoc --go_out=./curproto ./curproto/currency.proto

The previous command uses parameter --go_out to specify Go code generation. It will compile file currency.proto in directory curproto and place the generated Go code there as well. The compilation step will generate Go source file currency.pb.go which contains code for serialization, deserialization, and struct types matching the messages defined in the .proto file:

type Currency struct {
	Code    string `protobuf:"bytes,1,opt,name=code" json:"code,omitempty"`
	Name    string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"`
	Number  int32  `protobuf:"varint,3,opt,name=number" json:"number,omitempty"`
	Country string `protobuf:"bytes,4,opt,name=country" json:"country,omitempty"`
}
...
type CurrencyList struct {
	Items []*Currency `protobuf:"bytes,1,rep,name=items" json:"items,omitempty"`
}

Generated Go file pb-examples/curproto/currency.pb.go

Using Protobuf directly

Once we have the ability to serialize and deserialize our data, we can use it for any purpose where binary encoding can be applied. For instance, the following example shows how to use protocol buffer as an effeicient format for storage. The source snippet below loads data from a CSV file and saves it as a protocol buffer encoded file.

import (
    ...
    "github.com/golang/protobuf/proto"
    "github.com/vladimirvivien/go-grpc/pg-example/curproto"
)

const fileName = "data.pb"

func main() {
	currencyItems, err := createPbFromCsv("../curdata.csv")
	if err != nil {
		log.Fatalf("failed to load csv: %v\n", err)
	}

	// encode data as protobuf binary
	data, err := proto.Marshal(currencyItems)
	if err != nil {
		log.Fatal(err)
	}

	// save the encoded binary to file
	if err := ioutil.WriteFile(fileName, data, 0644); err != nil {
		log.Fatal(err)
	}
	log.Println("data file saved as", fileName)
}

// read csv content and return rows as *curproto.CurrencyList
// a type generated from protobuf
func createPbFromCsv(path string) (*curproto.CurrencyList, error) {
	items := make([]*curproto.Currency, 0)
	file, err := os.Open(path)
	if err != nil {
		return nil, err
	}
	defer file.Close()

	// create CSV reader from file
	reader := csv.NewReader(file)
	for {
		row, err := reader.Read()
		if err != nil {
			if err == io.EOF {
				break
			} else {
				return nil, err
			}
		}
		var num int32
		if i, err := strconv.Atoi(row[3]); err == nil {
			num = int32(i)
		}
		// copy row data into protobuf-generated type
		c := &curproto.Currency{
			Country: row[0],
			Name:    row[1],
			Code:    row[2],
			Number:  num,
		}
		items = append(items, c)
	}
	return &curproto.CurrencyList{Items: items}, err
}

Protobuf example file pb-examples/encode_pb.go

When the code (above) is executed, it will produce file data.pb with the data encoded using the protocol buffer binary format. Doing something similar using JSON (source code), we can compare the resulting data file sizes for a rough comparison in efficiency as shown below:

-rw-r--r--  1 vvivien  staff    20K  data.js
-rw-r--r--  1 vvivien  staff    10K  data.pb

This simple test reveals that the protobuf-encoded file is half the size of the JSON-encoded file. This saving can be even more pronounced when the data is composed of mostly numeric data.

Creating Services with Protobuf and gRPC

Earlier we have seen how protocol buffers work. Now, let us see how to use protobuf and the gPRC framework to build efficient and fast RPC services. When creating services with gRPC, there are three general steps that must be followed:

  1. Create a protobuf file (IDL) to define messages and service methods
  2. Compile the protobuf IDL into code to generate types and service interfaces
  3. Implement the code for the service remote methods

1. Define Protocol Buffers IDL

Using the currency service scenario, presented earlier, let us define the protobuf file that contains the messages and the service definition:

syntax = "proto3";
  
message Currency {
    string code = 1; 
    string name = 2;
    int32 number = 3;
    string country = 4;
}

message CurrencyList {
    repeated Currency items = 1;
}

message CurrencyRequest {
    string code = 1;
    int32 number = 2;
}

// CurrencyService exposes methods to call
service CurrencyService {
    rpc GetCurrencyList(CurrencyRequest) returns (CurrencyList){}
}

Protocol Buffers IDL protobuf/currency.proto

The protocol buffer defines the messages that we saw earlier. However, it now also contains a service block which defines one or more rpc methods. In our example, service CurrencyService :

  • Offers method GetCurrencyList
  • The method takes CurrencyRequest as input paremeter
  • And, returns CurrencyList to the client

2. Compile the IDL File

As before, the protobuf IDL file needs to be compiled into source code. To generate code for gPRC, however, we need to specify additional protoc parameters to trigger the gRPC plugin which will generate the code necessary to bootstrap the RPC services and remote methods.

Assuming the IDL file above is located in a folder called ./protobuf, the following will generate, in addition to the message types, the gRPC code needed to implement remote server methods and the client stubs to call them:

 $ protoc -I=./protobuf --go_out=plugins=grpc:./protobuf ./protobuf/currency.proto

Notice the additional parameter value in --go_out

The compiler will genearate file currency.pb.go in the ./protobuf directory. The generated source contains the message struct types (as before), but also includes the service methods as a Go interface to be implemented:

type Currency struct {
	Code    string `protobuf:"bytes,1,opt,name=code" json:"code,omitempty"`
	Name    string `protobuf:"bytes,2,opt,name=name" json:"name,omitempty"`
	Number  int32  `protobuf:"varint,3,opt,name=number" json:"number,omitempty"`
	Country string `protobuf:"bytes,4,opt,name=country" json:"country,omitempty"`
}

type CurrencyList struct {
	Items []*Currency `protobuf:"bytes,1,rep,name=items" json:"items,omitempty"`
}

type CurrencyRequest struct {
	Code   string `protobuf:"bytes,1,opt,name=code" json:"code,omitempty"`
	Number int32  `protobuf:"varint,2,opt,name=number" json:"number,omitempty"`
}

// service interface for CurrencyService
type CurrencyServiceServer interface {
	GetCurrencyList(context.Context, *CurrencyRequest) (*CurrencyList, error)
}

3. Implement the service

The last general step is to implement remote methods for the service. For our this example, we will implement method GetCurrencyList() which return a value of type CurrencyList as defined in the IDL.

import (
	"golang.org/x/net/context"
	"google.golang.org/grpc"

	pb "github.com/vladimirvivien/go-grpc/protobuf"
	"github.com/vladimirvivien/go-grpc/util"
)

type CurrencyService struct {
	data []*pb.Currency
}

func newCurrencyService(data []*pb.Currency) *CurrencyService {
	return &CurrencyService{data: data}
}

// GetCurrencyList searches (by Code or Number) and return CurrencyList
func (c *CurrencyService) GetCurrencyList(
	ctx context.Context,
	req *pb.CurrencyRequest,
) (*pb.CurrencyList, error) {

	var items []*pb.Currency
	for _, cur := range c.data {
		if cur.GetNumber() == req.GetNumber() || cur.GetCode() == req.GetCode() {
			items = append(items, cur)
		}
	}

	return &pb.CurrencyList{Items: items}, nil
}

func main() {

	// load data into protobuf structures
	data, err := util.LoadPbFromCsv("./../curdata.csv")
	if err != nil {
		log.Fatal(err) // dont start
	}

    lstnr, err := net.Listen("tcp", ":50050")
	if err != nil {
		log.Fatal("failed to start server:", err)
	}

	// setup and register currency service
	curService := newCurrencyService(data)
	grpcServer := grpc.NewServer()
	pb.RegisterCurrencyServiceServer(grpcServer, curService)

	// start service's server
	log.Println("starting currency rpc service on", port)
	if err := grpcServer.Serve(lstnr); err != nil {
		log.Fatal(err)
	}
}

gRPC server file grpc/server.go

In function main, the code uses package grpc to register the service implementation and bootstrap the server to expose remote method GetCurrencyList(). When the code is executed, an HTTP/2 server will start listening for requests on TCP port 50050. This is the general pattern that is often used to get a gRPC service up and running.

Using the service

At this point, we are ready to write a simple client which can invoke the remote method defined earlier. The snippet below shows a Go client that calls the service.

It should be noted that the client stubs used to call the remote service are (can be) generated when the protocol buffer definition file is compiled (see above).

// printUSD demonstrates simple binary call from client
func printUSD(client pb.CurrencyServiceClient) {
	curReq := &pb.CurrencyRequest{Code: "USD"}
	curList, err := client.GetCurrencyList(context.Background(), curReq)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Println("\nUSD Countries")
	fmt.Println("-------------")
	for _, cur := range curList.Items {
		fmt.Printf("%-50s%-10s\n", cur.GetCountry(), cur.GetCode())
	}
}

func main() {
	serverAddr := net.JoinHostPort("localhost", "50050")

	// setup insecure connection
	conn, err := grpc.Dial(serverAddr, grpc.WithInsecure())
	if err != nil {
		log.Fatal(err)
	}

	client := pb.NewCurrencyServiceClient(conn)

	printUSD(client)
}

Go client file grpc/client.go

In function main() the code uses package grpc to setup connection to the RPC server. The client stub is generated during protoc compilation and provides an extensive API to communicate with the server. Note in function printUSD the call to client.GetCurrencyList() looks like it is a local call. However, its an abstraction that hides the complicated dance of serialization and deserialization of protocol buffers to communicat with the server.

Other gRPC Examples

This repository contains an extensive list of gRPC examples and Go. You may find some of the followings useful:

  • grpc_auth: example of implementation of JWT token-based authorization.
  • grpc_err: shows how to do error handling in gRPC including the use of complex error objects.
  • grpc_intrcpt: introduction to intercept for logging.
  • grpc_limits: shows how add preventive measures to guard your gRPC service and clients from failures by specifying limits.
  • grpc_rate: shows how to setup limits on the rate at which a service can be called for a given period of times.
  • grpc_retry: shows how to use intecerptors to implement a simple retry logic.
  • grpc_tls:shows how to setup TLS-based auth on both client and the server.
  • grpc_to: shows how to use context timeout to indicate to the framework how long a request should take.

About

A collection of gRPC and Go examples showcasing features of the framework

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • Go 100.0%