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, usingapt
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:
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 ourproto
, generated code files, and service implementationbin/
directory contains ourclient
andserver
executable filesclient/
directory contains our gRPC client implementation of the serviceserver/
directory contains our gRPC server implementation of the servicepkg/
directory contains packages for helper utilities i.e.cmdutil/command_runner.go
for running OS commandsgo.mod
is the module definition file since we use Go modules for dependency managementgo.sum
contains all the dependency checksums of thesystemservice
moduledebian/
directory containssystemd
service file as a referenceDockerfile
helps build a container image to run the server containerizedrun_server_cont.sh
script runs gRPC server within the container based on the directives at the beginning of theDockerfile
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 ofrpc
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
tellsprotoc
to use the gRPC plugin and put the generated files in theapi
directory./api/System.proto
specifies inputproto
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 theSystem
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 theSystemServer
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 aCommandRunner
member namedcmd
and fulfills actual work of the service by calling and parsing the return values of thecmd.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 ourSystem
service takesEmpty
as a parameter which is of thegoogle.protobuf.Empty
type. - Each method also takes a
Context
parameter. AContext
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 theSystem.proto
file. - Methods also return an
error
object along with the return values. We return anil
error to tell the gRPC that we have successfully processed the request. In case of an error, we return a non-nilerror
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 forSystem
service - Then, we create a TCP listener that we want the gRPC server to bind to the specified port within
addr
, by default to50001
- 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 forSystem
service - With
grpc.Dial
, we set up a connection with the server using the server addressserverAddr
- Then, we create a new gRPC client with
systempb.NewSystemClient(conn)
which passes theconn
connection to thestub
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.