Skip to content

gRPC Golang Tutorial

This tutorial aims to provide a quick introduction on how to develop a simple application using gRPC ecosystem using Golang. Even though we implement a client-server application here, you will have a good sense of the key concepts of the gRPC.

Overview

We will implement a gRPC service called System that is used to retrieve various system information, for example product name, distro info, kernel version, CPU usage and a client application that invokes the developed service.

In a nutshell, developing a gRPC service consists of the following steps:

  • Service definition in a .proto file
  • Code generation from the service definition
  • Implementing the server-side logic of this service definition
  • Implementing the client-side logic of this service definition

Development of the System gRPC service starts with speficying the System service definition as a protocol buffer definition in the System.proto file. After that, we generate service skeleton, System.pb.go, which contains the server-side and the client-side interface code by using the protocol buffer compiler protoc and the gRPC Go plugin.

Then, we proceed to the actual implementation of the gRPC service which indeed is the task of implementing the interfaces that are generated in the code generation step.

In server package, we implement the server-side business logic of the System service with the set of remote methods that we specified in the service definition. Then, we expose these remote method implementations by setting up and running a gRPC server that binds the System service.

In client package, we implement a gRPC client on top of the generated client stub that connects our gRPC server and invokes the remote methods exposed by the gRPC server.

Finally, we generate the binaries for both the server and client gRPC applications.

Prerequisities

Install Golang (v1.15)

To setup a go development environment on a Debian 10 (buster), open a terminal, and type the following commands:

curl -O https://dl.google.com/go/go1.15.linux-amd64.tar.gz
sudo tar -xzvf go1.15.linux-amd64.tar.gz -C /usr/local/
sudo chown -R root:root /usr/local/go
mkdir -p $HOME/go/{bin,src}
echo 'export GOPATH=$HOME/go' >> ~/.bashrc
echo 'export PATH=/usr/local/go/bin:$PATH:$GOPATH/bin:$GOROOT/bin' >> ~/.bashrc
source ~/.bashrc

The commands above should install Go v1.15 into your /usr/local/go directory. It will also create a Go workspace in your home directory at $HOME/go along with $HOME/go/src for keeping Go source code repositories and $HOME/go/bin for created Go binaries. We also set our PATH environment variable to include Go bin directories that allows you to run your Go programs in your shell.

Check go environment by running go env and ensure you have a similar output as following:

GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/home/industrial/.cache/go-build"
GOENV="/home/industrial/.config/go/env"
GOEXE=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="linux"
GOINSECURE=""
GONOPROXY=""
GONOSUMDB=""
GOOS="linux"
GOPATH="/home/industrial/workspace/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/usr/local/go"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/usr/local/go/pkg/tool/linux_amd64"
GCCGO="gccgo"
AR="ar"
CC="gcc"
CXX="g++"
CGO_ENABLED="1"
GOMOD=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -m64 -pthread -fmessage-length=0 -fdebug-prefix-map=/tmp/go-build023234558=/tmp/go-build -gno-record-gcc-switches"

Install Protocol Buffers (v3)

In order to generate the server and client interface code, we need to install the relevant tools which are protocol buffers and Go plugins for the protocol buffers.

  • Install protocol buffers compiler protoc version 3, using apt package manager:
sudo apt install protobuf-compiler libprotobuf-dev
  • Install Go plugins for the protocol compiler:
$ export GO111MODULE=on  # Enable module mode
$ go get google.golang.org/protobuf/cmd/protoc-gen-go \
        google.golang.org/grpc/cmd/protoc-gen-go-grpc

Finally, ensure that $GOPATH/bin is in your $PATH, so that the protoc compiler can find the plugins:

export PATH="$PATH:$(go env GOPATH)/bin"

Install Goreleaser (optional)

NOTICE

GoReleaser is a release automation tool for Go projects. The goal is to simplify the build, release and publish steps while providing variant customization options for all steps.

GoReleaser is built for CI tools, you only need to download and execute it in your build script. You can also install it locally if you wish.

You can also customize your release process through a .goreleaser.yml file.

We create Debian binary packages using the Goreleaser tool. Install Goreleaser issuing the following command:

curl -sfL https://install.goreleaser.com/github.com/goreleaser/goreleaser.sh | sh

Getting the Example Code

Download the tutorials from the following link:

IEDK TutorialsIEDK Tutorials

Then extract the archive:

tar xzvf tutorials.tar.gz

Then, change your current directory to golang-samples/systemservice:

cd golang-samples/systemservice

The project directory should look as following:

systemservice
├── api
   ├── handler.go
   ├── System.pb.go
   └── System.proto
├── bin
   ├── client
   └── server
├── client
   └── main.go
├── debian
   └── iedk-system.service
├── Dockerfile
├── go.mod
├── go.sum
├── Makefile
├── pkg
   └── cmdutil
       └── command_runner.go
├── run_server_cont.sh
└── server
    └── main.go

7 directories, 15 files
  • api/ directory contains our proto, generated code files, and service implementation
  • bin/ directory contains our client and server executable files
  • client/ directory contains our gRPC client implementation of the service
  • server/ directory contains our gRPC server implementation of the service
  • pkg/ directory contains packages for helper utilities i.e. cmdutil/command_runner.go for running OS commands
  • go.mod is the module definition file since we use Go modules for dependency management
  • go.sum contains all the dependency checksums of the systemservice module
  • debian/ directory contains systemd service file as a reference
  • Dockerfile helps build a container image to run the server containerized
  • run_server_cont.sh script runs gRPC server within the container based on the directives at the beginning of the Dockerfile
  • Makefile provides various targets that automates the development of the project

Makefile

Our sample project comes with a handy Makefile to accomplish various tasks to be used within the development, i.e. code generation, compile client and server code, get dependencies, clean workspace, produce Debian package, perform docker image tasks.

Run make help for all of the available targets:

$ make help
all                            Run build, test, vet, lint and fmt
api                            Auto-generate grpc go sources
build                          Build both api, server and client
clean                          Remove previous builds
client                         Build the binary file for client
deb                            Build Debian package (deb)
dep                            Get the dependencies
docker-image                   Build docker image
docker-push                    Push docker image to registry
help                           Display this help screen
server                         Build the binary file for server
test                           Run all the tests

Dockerfile

We have a sample Dockerfile which demonstrates how to build and run a gRPC-go project using Docker containers. You can easily call run_server_cont.sh script to run our gRPC server within the container you would create with make docker-image. This script reads the directives at the top of the Dockerfile and runs the container according to those directives.

Debian Package

By using make deb target within the Makefile, you can create the Debian package of our sample project containing the executables of our client and server packages as well as the debian/iedk-system.service service file to be deployed as a systemd service at the Debian-based target platforms.

Ensure that you have installed goreleaser as mentioned in the Prerequisities section to create Debian packages.

Development

Service Definition

For defining a gRPC service interface between the client and the server, we use message and service types from protocol buffers:

  • message type is a data structure that is exchanged between the client and the service

  • service type is a collection of rpc methods that are exposed to a client

In service definitions, every RPC method has an input and output type and message types are used to define these input and output types of the RPC methods.

There are 4 different kinds of RPC methods that gRPC lets us define in our services:

  • Unary RPC: Client sends a single request to the server and gets a single response in remote method invocation
  • Server-Streaming RPC: Server sends back a sequence of responses after getting client's request
  • Client-Streaming RPC: Client sends a sequence of requests to the server and gets back a single response
  • Bidirectional-Streaming RPC: Both client and server send each other a sequence of request and responses independently

In our System gRPC service, we use the following System.proto file which defines the messages being sent and the RPC methods for our systemservice gRPC service:

syntax = "proto3";
import "google/protobuf/empty.proto";
option go_package = ".;api";
package api;

// Product message represents system product name
message Product{
    string name = 1;        //  Virtualbox
}

// Distro message represents distribution information
message Distro {
    string id = 1;          // debian
    string release = 2;      // 10
    string codename = 3;    // buster
}

// Kernel message represents operationg system and kernel version
message Kernel {
    string os = 1;          // GNU/Linux
    string release = 2;     // 4.19.0-12-amd64
}

// Baseboard message represents board information
message Baseboard {
    string vendor = 1;   // board vendor
    string name = 2;     // board name
    string chipset = 3;  // board chipset
}

// Memory message represents memory usage stats in human friendly size
message Memory {
    string total = 1;        // mem total
    string used = 2;         // mem used
    string free = 3;         // mem free
    string available = 4;    // mem available
}

// Cpu message represents CPU model and load average
message Cpu{
    string name = 1;        //  CPU model name
    string load = 2;        //  CPU load
}

// Disk message represents disk model and capacity information
message Disk{
    string name = 1;        //  Disk name
    string path = 2;        //  Disk path
    string size = 3;        //  Disk size
    string used = 4;        //  Disk used
    string avail = 5;       //  Disk available
    string percent = 6;     //  Disk usage percentega
}

// System service provides various information about the platform
service System {
    rpc GetProductInfo(google.protobuf.Empty) returns(Product);
    rpc GetDistroInfo(google.protobuf.Empty) returns(Distro);
    rpc GetKernelInfo(google.protobuf.Empty) returns(Kernel);
    rpc GetBaseboardInfo(google.protobuf.Empty) returns(Baseboard);
    rpc GetCPUInfo(google.protobuf.Empty) returns(Cpu);
    rpc GetMemoryInfo(google.protobuf.Empty) returns(Memory);
    rpc GetDiskInfo(google.protobuf.Empty) returns(Disk);
}

Code Generation

After defining the protofile, we need to compile the protofile by using the protocol buffers compiler protoc to generate the server and the client interfaces. These interfaces implement the gRPC code which the applications will use:

protoc --go_out=plugins=grpc:api ./api/System.proto
  • protoc is the protocol buffer compiler
  • --go_out=plugins=grpc:api tells protoc to use the gRPC plugin and put the generated files in the api directory
  • ./api/System.proto specifies input proto file used for code generation

Running the above command will produce the System.pb.go file which contains the following:

  • Protocol buffers code to populate, serialize and retrieve the message types defined in the System service
  • An interface type for clients (client-side stub) code to call the methods defined in the System service
  • An interface type for servers (server-side stub) to implement the methods defined in the System service

Once you define the gRPC service (System) and generate gRPC stubs with protoc by using the gRPC Go plugin, the next step is to implement the business logic of our System service. To achieve that, we need to write our own System service server and client components which will contain the actual implementations of the interfaces that are generated in this step.

Server Implementation

Server-side development of a gRPC service requires to accomplish the following tasks:

  • Implementing the logic of the service interface which is the set of remote methods defined in the service definiton
  • Running a gRPC server and register the service to handle client requests

Implementing business logic

For System service, we implement the remote methods in api/handler.go file as shown below:

package api

import (
 "bufio"
 "bytes"
 "context"
 "fmt"
 "log"
 "os"
 "runtime"
 "strings"

 "google.golang.org/grpc/codes"
 "google.golang.org/grpc/status"

 cmdutil "systemservice/pkg/cmdutil"

 emptypb "github.com/golang/protobuf/ptypes/empty"
)

var (
 // ErrNotFound error type
 ErrNotFound = status.Errorf(codes.NotFound, "Service not found")
 // ErrServiceUnimplemented error type
 ErrServiceUnimplemented = status.Errorf(codes.Unimplemented, "Service not implemented")
)

// Server represents the gRPC server
// Allows to attach some resources to your server,
// making them available during the RPC calls
type Server struct {
 UnimplementedSystemServer
 cmd cmdutil.CommandOperations
}

// NewServer creates an instance of Server
func NewServer() *Server {
 s := &Server{cmd: &cmdutil.CommandRunner{}}

 return s
}

// GetProductInfo returns system product name
func (s *Server) GetProductInfo(ctx context.Context, request *emptypb.Empty) (*Product, error) {
 out, err := s.cmd.GetCommandOutput("cat", "/sys/devices/virtual/dmi/id/product_name")
 if err != nil {
  return nil, fmt.Errorf("failed to get product name: %v", err)
 }

 productName := string(bytes.TrimSpace(out))

 return &Product{Name: productName}, nil
}

// GetCPUInfo returns CPU model and usage
func (s *Server) GetCPUInfo(ctx context.Context, request *emptypb.Empty) (*Cpu, error) {
 info, err := s.cmd.GetCommandOutput("bash", "-c", "lscpu | grep 'Model name' | cut -d ':' -f2")
 if err != nil {
  return nil, fmt.Errorf("failed to get CPU information: %v", err)
 }
 cpuinfo := string(bytes.TrimSpace(info))

 load, err := s.cmd.GetCommandOutput("bash", "-c", "top -bn1 -p0 | awk '/^%Cpu/{print 100-$8}'")
 if err != nil {
  return nil, fmt.Errorf("failed to get CPU load: %v", err)
 }
 cpuload := string(bytes.TrimSpace(load))

 return &Cpu{Name: cpuinfo, Load: cpuload}, nil
}

// GetDistroInfo returns distribution info string in id, release, codename format
func (s *Server) GetDistroInfo(ctx context.Context, request *emptypb.Empty) (*Distro, error) {
 var distro string
 if runtime.GOOS == "linux" {
  osReleasePath := "/etc/os-release"
  if _, err := os.Stat(osReleasePath); err == nil {
   props, err := parseOsReleaseFile(osReleasePath)
   if err != nil {
    log.Fatalf("error: can't read os-release file - %s", err)
   }

   id := props["ID"]
   release := props["VERSION_ID"]
   codename := props["VERSION_CODENAME"]

   distro = fmt.Sprintf("%s %s %s", strings.Title(id), release, strings.Title(codename))
  } else {
   return nil, fmt.Errorf("Could not read os-release file")
  }
 } else {
  return nil, fmt.Errorf("Non-GNU/Linux operating system")
 }

 fields := strings.Fields(distro)
 if len(fields) == 0 {
  return &Distro{}, nil
 }

 if len(fields) != 3 {
  return nil, fmt.Errorf("Invalid distribution info string")
 }

 id := fields[0]
 release := fields[1]
 codename := fields[2]

 return &Distro{Id: id, Release: release, Codename: codename}, nil
}

// GetKernelInfo returns kernel version along with operating system
func (s *Server) GetKernelInfo(ctx context.Context, request *emptypb.Empty) (*Kernel, error) {
 out, err := s.cmd.GetCommandOutput("uname", "-or")
 if err != nil {
  return nil, fmt.Errorf("failed to get kernel version: %v", err)
 }

 kernelInfo := string(bytes.TrimSpace(out))

 fields := strings.Fields(kernelInfo)
 if len(fields) == 0 {
  return &Kernel{}, nil
 }

 if len(fields) != 2 {
  return nil, fmt.Errorf("Invalid kernel version string")
 }
 os := fields[0]
 release := fields[1]

 return &Kernel{Os: os, Release: release}, nil
}

// GetBaseboardInfo returns system board information
func (s *Server) GetBaseboardInfo(ctx context.Context, request *emptypb.Empty) (*Baseboard, error) {
 out, err := s.cmd.GetCommandOutput("cat", "/sys/devices/virtual/dmi/id/board_vendor")
 if err != nil {
  return nil, fmt.Errorf("failed to get product vendor information: %v", err)
 }

 vendor := string(bytes.TrimSpace(out))

 out, err = s.cmd.GetCommandOutput("cat", "/sys/devices/virtual/dmi/id/product_name")
 if err != nil {
  return nil, fmt.Errorf("failed to get product name information: %v", err)
 }

 name := string(bytes.TrimSpace(out))

 out, err = s.cmd.GetCommandOutput("bash", "-c", "lspci | grep ISA | sed -e \"s/.*: //\" -e \"s/LPC.*//\" -e \"s/Controller.*//\"")
 if err != nil {
  return nil, fmt.Errorf("failed to get chipset information: %v", err)
 }

 chipset := string(bytes.TrimSpace(out))

 return &Baseboard{Vendor: vendor, Name: name, Chipset: chipset}, nil
}

// GetMemoryInfo returns system memory usage information
func (s *Server) GetMemoryInfo(ctx context.Context, request *emptypb.Empty) (*Memory, error) {
 out, err := s.cmd.GetCommandOutput("free", "-h", "--si")
 if err != nil {
  return nil, fmt.Errorf("failed to get memory version: %v", err)
 }

 r := bytes.NewReader(bytes.TrimSpace(out))
 scanner := bufio.NewScanner(r)
 var memoryRow string
 for scanner.Scan() {
  line := scanner.Text()
  if line != "" && strings.HasPrefix(line, "Mem:") {
   memoryRow = line
   break
  }
 }

 fields := strings.Fields(memoryRow)
 if len(fields) == 0 {
  return nil, fmt.Errorf("sysinfo: Invalid memory usage info")
 }

 fields = fields[1:] // Get rid of "Mem:" part

 total := fields[0]
 used := fields[1]
 free := fields[2]
 avail := fields[5]

 return &Memory{
  Total:     total,
  Used:      used,
  Free:      free,
  Available: avail}, nil
}

// GetDiskInfo returns disk model and capacity information
func (s *Server) GetDiskInfo(ctx context.Context, request *emptypb.Empty) (*Disk, error) {
 out, err := s.cmd.GetCommandOutput("cat", "/sys/block/sda/device/model")
 if err != nil {
  return nil, fmt.Errorf("failed to get disk vendor information: %v", err)
 }
 name := string(bytes.TrimSpace(out))

 out, err = s.cmd.GetCommandOutput("lsblk", "-ndp", "-I", "8", "-o", "NAME,SIZE")
 if err != nil {
  return nil, fmt.Errorf("failed to get disk information: %v", err)
 }
 ret := string(bytes.TrimSpace(out))
 fields := strings.Fields(ret)
 if len(fields) != 2 {
  return nil, fmt.Errorf("Bad line in disk info")
 }
 path := fields[0]
 size := fields[1]

 return &Disk{Name: name, Path: path, Size: size}, nil
}

// Helper method to parse the OS release file
func parseOsReleaseFile(path string) (map[string]string, error) {
 file, err := os.Open(path)
 if err != nil {
  return nil, err
 }
 defer file.Close()

 props := make(map[string]string)
 scanner := bufio.NewScanner(file)
 for lnum := 1; scanner.Scan(); lnum++ {
  fields := strings.Split(scanner.Text(), "=")
  if len(fields) != 2 {
   return nil, fmt.Errorf("%s:%d bad line", path, lnum)
  }
  props[fields[0]] = strings.Trim(fields[1], "\"")
 }

 if err := scanner.Err(); err != nil {
  return nil, err
 }

 return props, nil
}
  • Within SytemInfo service implementation, we employ only one type of gRPC communication pattern to keep the tutorial simple: Unary RPC or Simple RPC which means that gRPC clients sends a single request to the gRPC server and receives a single response from the server.
  • Server implementation starts by importing the systempb package which contains the generated code.
  • Then we define a server struct which is an abstraction of the server. It implements the SystemServer interface from the generated code and allows attaching actual service implementations to the gRPC server.
  • We employ another struct, called CommandRunner which stands for an abstraction of various helpers that are used to run OS commands. server has a CommandRunner member named cmd and fulfills actual work of the service by calling and parsing the return values of the cmd.Run() command under the hood.
  • In gRPC, rpc methods must have exactly 1 input message and exactly 1 output message even though your methods do not have any input or output paramater. For that reason, every method in our System service takes Empty as a parameter which is of the google.protobuf.Empty type.
  • Each method also takes a Context parameter. A Context object exists during the lifetime of the request and contains metadata such as identity of the end user, authorization tokens and the request's deadline.
  • Methods return the appropriate protocol buffer struct as a response. Response structs are defined in the System.pb.go file which is autogenerated from the System.proto file.
  • Methods also return an error object along with the return values. We return a nil error to tell the gRPC that we have successfully processed the request. In case of an error, we return a non-nil error with an explanatory message. Then, the gRPC layer translates these errors into an appropriate RPC status and propagates them to the service consumers to be used for error handling on their side.

Running a gRPC server

After finishing the service implementation, we need to run a gRPC server to listen for requests from clients, dispatch those requests to the service implementation and return the service responses back to the client.

The following snippet shows how we run a gRPC server that binds System service in server/main.go:

package main

import (
 "flag"
 "log"
 "net"
 "os"

 "google.golang.org/grpc"

 systempb "systemservice/api"
)

func main() {
 flags := struct {
  address    string
  socketAddr string
 }{}
 flag.StringVar(&flags.address, "address", "", "TCP endpoint to listen on for gRPC connections ie. '127.0.0.1:50001'.")
 flag.StringVar(&flags.socketAddr, "socket", "", "Unix Domain Socket endpoint to listen on for gRPC connections ie. '/tmp/test.sock'.")

 // parse command-line arguments
 flag.Parse()

 // Create a system service server instance
 s := systempb.NewServer()

 // Create a gRPC server object
 grpcServer := grpc.NewServer()

 // Attach the system service server to the gRPC server
 systempb.RegisterSystemServer(grpcServer, s)

 // Start a gRPC server listening on TCP
 if flags.address != "" {
  log.Printf("starting gRPC server on %s", flags.address)
  // Create a TCP listener on the given port (default 50001) to bind our gRPC server
  lis, err := net.Listen("tcp", flags.address)
  if err != nil {
   log.Fatalf("failed to start listening: %v", err)
  }

  // Start the gRPC server in a goroutine
  if err := grpcServer.Serve(lis); err != nil {
   log.Fatalf("failed to serve: %v", err)
  }
  defer grpcServer.Stop()
 }

 // Start a gRPC server listening on a unix domain socket
 if flags.socketAddr != "" {
  log.Printf("starting gRPC server on %s", flags.socketAddr)
  // Clean up any previously created socket file
  if err := os.Remove(flags.socketAddr); err != nil && !os.IsNotExist(err) {
   log.Fatalf("failed to remove stalled socket file %v", err)
  }

  lis, err := net.Listen("unix", flags.socketAddr)
  if err != nil {
   log.Fatalf("failed to start listening: %v", err)
  }

  // Start the gRPC server
  if err := grpcServer.Serve(lis); err != nil {
   log.Fatalf("failed to serve: %v", err)
  }
  defer grpcServer.Stop()
 }
}

Let's break down the code briefly to demistify how we build and run a simple server that will expose our service methods and accept messages from gRPC clients:

  • First, we import systempb package that contains the generated code for System service
  • Then, we create a TCP listener that we want the gRPC server to bind to the specified port within addr, by default to 50001
  • Then, we create an instance of the gRPC server using gRPC Go APIs
  • Then, we register our System service implementation with the gRPC server using the generated APIs
  • Finally, we start listening for incoming messages with the grpcServer.Serve() call

Client Implementation

The final step in this tutorial is creating a gRPC client application that will connect to our gRPC server and call the service methods that it exposes.

The following snippet shows how we create a simple gRPC client in client/main.go:

// Print a friendly greeting

package main

import (
 "context"
 "flag"
 "log"
 "net"
 "net/url"
 "strings"

 "google.golang.org/grpc"

 systempb "systemservice/api"

 emptypb "github.com/golang/protobuf/ptypes/empty"
)

func main() {
 flags := struct {
  endpoint string
 }{}
 flag.StringVar(&flags.endpoint, "endpoint", "http://127.0.0.1:50001", "gRPC server address (TCP or Unix Domain Sockets)")

 // parse command-line arguments
 flag.Parse()

 if flags.endpoint == "" {
  log.Fatal("server address not provided")
 }

 // Parse server endpoint, get either TCP or socket address schema
 protocol, host, _ := ParseEndpoint(flags.endpoint)
 var dialOpts []grpc.DialOption
 if protocol == "unix" {
  dialOpts = append(dialOpts, grpc.WithInsecure())
  dialOpts = append(dialOpts, grpc.WithContextDialer(func(ctx context.Context, addr string) (net.Conn, error) {
   return net.Dial("unix", addr)
  }))
 } else {
  dialOpts = append(dialOpts, grpc.WithInsecure())
 }

 // Make a client connection to the gRPC server
 conn, err := grpc.Dial(host, dialOpts...)
 if err != nil {
  log.Printf("failed to dial server %s: %v", flags.endpoint, err)
 }
 defer conn.Close()

 // Create a gRPC client
 client := systempb.NewSystemClient(conn)

 // Contact the server and print out its response.
 ctx := context.Background()

 // Call remote methods exposed by the gRPC server
 product, err := client.GetProductInfo(ctx, &emptypb.Empty{})
 if err != nil {
  log.Fatalf("systemservice: error: %v", err)
 }
 log.Printf("Product\t: %v", product.Name)

 distro, err := client.GetDistroInfo(ctx, &emptypb.Empty{})
 if err != nil {
  log.Fatalf("system-client: error: %v", err)
 }
 log.Printf("Distro\t: %s %s %s", distro.Id, distro.Release, distro.Codename)

 kernel, err := client.GetKernelInfo(ctx, &emptypb.Empty{})
 if err != nil {
  log.Fatalf("system-client: error: %v", err)
 }
 log.Printf("Kernel\t: %s %s", kernel.Os, kernel.Release)

 board, err := client.GetBaseboardInfo(ctx, &emptypb.Empty{})
 if err != nil {
  log.Fatalf("system-client: error: %v", err)
 }
 log.Printf("Board\t: %s %s %s", board.Vendor, board.Name, board.Chipset)

 cpu, err := client.GetCPUInfo(ctx, &emptypb.Empty{})
 if err != nil {
  log.Fatalf("systemservice: error: %v", err)
 }
 log.Printf("CPU\t\t: %s , load: %s", cpu.Name, cpu.Load)

 memory, err := client.GetMemoryInfo(ctx, &emptypb.Empty{})
 if err != nil {
  log.Fatalf("system-client: error: %v", err)
 }
 log.Printf("Memory\t: %s total, %s used, %s free, %s avail", memory.Total, memory.Used, memory.Free, memory.Available)

 disk, err := client.GetDiskInfo(ctx, &emptypb.Empty{})
 if err != nil {
  log.Fatalf("system-client: error: %v", err)
 }
 log.Printf("Disk\t: %s %s %s", disk.Name, disk.Path, disk.Size)
}

// ParseEndpoint endpoint parses an endpoint of the form
// (http|https)://<host>*|(unix|unixs)://<path>)
// and returns a protocol ('tcp' or 'unix'),
// host (or filepath if a unix socket),
// scheme (http, https, unix, unixs).
// Borrowed directly from go.etcd.io/etcd/v3/clientv3/balancer/resolver/endpoint
func ParseEndpoint(endpoint string) (proto string, host string, scheme string) {
 proto = "tcp"
 host = endpoint
 url, uerr := url.Parse(endpoint)
 if uerr != nil || !strings.Contains(endpoint, "://") {
  return proto, host, scheme
 }
 scheme = url.Scheme

 // strip scheme:// prefix since grpc dials by host
 host = url.Host
 switch url.Scheme {
 case "http", "https":
 case "unix", "unixs":
  proto = "unix"
  host = url.Host + url.Path
 default:
  proto, host = "", ""
 }
 return proto, host, scheme
}

Let's break down the code which creates a client and invokes remote methods:

  • First, we import systempb package that contains the generated code for System service
  • With grpc.Dial, we set up a connection with the server using the server address serverAddr
  • Then, we create a new gRPC client with systempb.NewSystemClient(conn) which passes the conn connection to the stub client
  • Before invoking remote methods by using the client, we create a Context object to pass with the remote calls
  • Finally, we perform RPCs with client as calling a local method

Run the example

Ensure you are in the directory where you downloaded the examples i.e. golang-samples/systemservice.

Communication over Unix Domain Sockets

  • Open a terminal and run the gRPC server:
go run server/main.go --socket /tmp/test.sock
  • Open another terminal, run the gRPC client and observe the output:
$ go run client/main.go -endpoint unix:///tmp/test.sock
2020/11/23 15:51:49 Product     : VMware Virtual Platform
2020/11/23 15:51:49 Distro      : Debian 10 Buster
2020/11/23 15:51:49 Kernel      : 4.19.0-12-amd64 GNU/Linux
2020/11/23 15:51:49 Board       : Intel Corporation VMware Virtual Platform Intel Corporation 82371AB/EB/MB PIIX4 ISA (rev 08)
2020/11/23 15:51:49 CPU         : Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz , load: 26.2
2020/11/23 15:51:49 Memory      : 16G total, 6.2G used, 746M free, 9.6G avail
2020/11/23 15:51:49 Disk        : VMware Virtual I /dev/sda 200G

Communication over TCP

  • Open a terminal and run the gRPC server:
go run server/main.go --address :50001
  • Open another terminal, run the gRPC client and observe the output:
$ go run client/main.go -endpoint http://localhost:50001
2020/11/23 15:56:03 Product     : VMware Virtual Platform
2020/11/23 15:56:03 Distro      : Debian 10 Buster
2020/11/23 15:56:03 Kernel      : 4.19.0-12-amd64 GNU/Linux
2020/11/23 15:56:03 Board       : Intel Corporation VMware Virtual Platform Intel Corporation 82371AB/EB/MB PIIX4 ISA (rev 08)
2020/11/23 15:56:04 CPU         : Intel(R) Core(TM) i7-8850H CPU @ 2.60GHz , load: 3.3
2020/11/23 15:56:04 Memory      : 16G total, 6.2G used, 821M free, 9.6G avail
2020/11/23 15:56:04 Disk        : VMware Virtual I /dev/sda 200G

Conclusion

Developing a gRPC application is based on the idea of defining a service, specifying the methods that can be invoked remotely with their parameters and returning types. By default, gRPC uses protocol buffers as the Interface Definition Language (IDL) for describing both the service interface and the structure of the payload messages.