rpc

package module
v1.0.16 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Dec 14, 2019 License: Apache-2.0 Imports: 8 Imported by: 3

README

GOPI Go Language Application Framework

CircleCI

This respository contains remote procedure call (RPC) and service discovery modules for gopi. It supports gRPC and mDNS at present. This README guide will walk you though:

  • Satisfying dependencies
  • Building the helloworld service and client
  • Understanding how to use an existing client in your application
  • Generating a protocol buffer file for your service
  • Creating a new service and client

Please also see documentation for:

Introduction

A "microservice" is a server-based process which can satisfy remote procedure calls, by accepting requests, processing the information within the service, and providing a response. A "simple" microservice might provide a single response to a request, a more complicated version will accept requests in a "stream" and may provide responses similarly in a "stream".

Protocol Buffers are an attractive mechanism for defining a schema for this request and response, and can generate both the client and server code programmatically in many languages using a compiler. The Google gRPC project is a useful counterpart for the compiler, providing supporting libraries, but there are others such as Twerp which can be used to provide a more traditional REST-based interface on compiling the protocol buffer code.

When you have your service running on your network, how do other processes discover it? This is where the discovery mechanisms come into play. For local area networks, discovery by DNS provides an easy mechansism, specifically using multicast DNS and the DNS-SD protocol. For cloud environents, service registration and discovery through services like Consul.

Dependencies

It is assumed you're using either a MacOS or Debian Linux machine. For MacOS, you should be using the Homebrew Package Manager:

bash% brew install protobuf
bash% go get -u github.com/golang/protobuf/protoc-gen-go

For Debian Linux:

bash# sudo apt install protobuf-compiler
bash# sudo apt install libprotobuf-dev
bash# go get -u github.com/golang/protobuf/protoc-gen-go

You can then use the protoc compiler command with the gRPC plugin to generate golang code for client and server.

The "helloworld" service

As an example, the Greeter service provides a call SayHello which takes a name parameter and returns a greeting message. The definition of the service is available in the folder rpc/protobuf/helloworld.proto:

package gopi;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply);
}

message HelloRequest {
  string name = 1;
}
message HelloReply {
  string message = 1;
}

The fully qualified name of the service is gopi.Greeter. The golang code to construct the client and service can be generated with the make protobuf command:

bash% make protobuf
go generate -x ./rpc/...
protoc helloworld/helloworld.proto --go_out=plugins=grpc:.

This will create the generated client and server code in the file rpc/protobuf/helloworld.pb.go. There are some other services defined in the rpc/protobuf folder:

  • gopi.Version returns version numbers of the running service, service and host uptime.
  • gopi.Discovery returns service records for any discovered and registered services on the local area network.

The make protobuf command generates the client and server code for these as well. In order to generate the helloworld client and service binaries, use the following commands:

bash% make helloworld-service
bash% make helloworld-client

You can run the helloworld service with unencrypted requests and responses:

bash% helloworld-service -rpc.port 8080 -verbose
[INFO] Waiting for CTRL+C or SIGTERM to stop server

Then to communicate with the service, use the following command in a separate terminal window:

bash% helloworld-client -addr localhost:8080 -rpc.insecure
Service says: "Hello, World"
bash% helloworld-client -addr localhost:8080 -rpc.insecure -name David
Service says: "Hello, David"

You can use encrypted communications if you provide an SSL key and certificate. In order to generate a self-signed certificate, use the following commands, replacing DAYS, OUT and ORG with appropriate values:

bash% DAYS=99999
bash% OUT="${HOME}/.ssl"
bash% ORG="mutablelogic"
bash% install -d ${OUT} && openssl req \
  -x509 -nodes \
  -newkey rsa:2048 \
  -keyout "${OUT}/selfsigned.key" \
  -out "${OUT}/selfsigned.crt" \
  -days "${DAYS}" \
  -subj "/C=GB/L=London/O=${ORG}"

Then the following commands are used to invoke the service:

bash% helloworld-service -rpc.port 8080 \
  -rpc.sslkey ${OUT}/selfsigned.key -rpc.sslcert  ${OUT}/selfsigned.crt \
  -verbose
[INFO] Waiting for CTRL+C or SIGTERM to stop server

You can then drop the -rpc.insecure flag when invoking the client.

Getting service information

The helloworld client can also report what services are running remotely, using an argument. For example,

bash% helloworld-client -addr rpi3plus:44453 services
+------------------------------------------+
|                 SERVICE                  |
+------------------------------------------+
| gopi.Greeter                             |
| gopi.Version                             |
| grpc.reflection.v1alpha.ServerReflection |
+------------------------------------------+

To invoke the gopi.Version service with the client (which returns some information about the remote server):

bash% helloworld-client -addr rpi3plus:44453 version
+---------------+------------------------------------------+
|      KEY      |                  VALUE                   |
+---------------+------------------------------------------+
| goversion     | go1.12.1                                 |
| gittag        | v1.0.5-7-gb433a3b                        |
| hostname      | rpi3plus                                 |
| execname      | helloworld-service                       |
| servicename   | gopi                                     |
| githash       | b433a3bed938a452d874dc6df4c50ab8ba15e036 |
| serviceuptime | 14m2s                                    |
| gobuildtime   | 2019-04-28T16:43:54Z                     |
| gitbranch     | v1                                       |
+---------------+------------------------------------------+

The "dns-discovery" command

Often microservices are "discovered" on the network, rather than known about in advance. Discovery can be through DNS or through cached knowledge of what microservices are running. The dns-discovery command allows you to retrieve information about services through DNS. To make the command and discover services on the local network:

bash% make dns-discovery
bash% dns-discovery -timeout 2s
+---------------------+
|       SERVICE       |
+---------------------+
| _adisk._tcp         |
| _apple-mobdev2._tcp |
| _http._tcp          |
| _googlerpc._tcp     |
| _googlecast._tcp    |
| _googlezone._tcp    |
| _sftp-ssh._tcp      |
+---------------------+

This command returns any discovered service names provided within two seconds, for example, _sftp-ssh._tcp indicates there are SFTP services on the network. Service instances can then be looked up:

bash% dns-discovery -timeout 2s _sftp-ssh._tcp
+----------------+--------------+-------------------+--------------------------------+
|    SERVICE     | NAME         | HOST              |               IP               |
+----------------+--------------+-------------------+--------------------------------+
| _sftp-ssh._tcp | MacBook      | MacBook.local.:22 | 192.168.1.1                    |
|                |              |                   | fe80::10f7:b0b1:81cb:3e7b      |
|                |              |                   | fd00::1:1c20:4b27:3d42:1082    |
+----------------+--------------+-------------------+--------------------------------+

In this example there is a host called "MacBook" providing a service instance on port 22. The IP addresses listed can be used to connect to SFTP. You can also keep the command running to cache records from the network. For example:

bash% dns-discovery -timeout 2s -watch -dns-sd.db cache.json
ADDED      _ipp._tcp                      Brother\ HL-3170CDW\ series (brother-eth.local.:631)

The command will stream the records onto your screen, either ADDED, UPDATED, EXPIRED or REMOVED. Press CTRL+C to interrupt. A file cache.json will be maintained in your home folder which contains discovered service instances on the network, whilst the command is running.

Using a client in your own application

If you want to use a client in your own application, you'll need to do the following:

  1. Know the DNS service name or address & port you want to connect to;
  2. Use a "client pool" to lookup and return a service record;
  3. Connect to the remote service instance using the service record;
  4. If you're connecting to a gRPC service instance, know the name of the gRPC service;
  5. Create a gRPC client with the connection;
  6. Use the client to call remote service methods.

The RPCClientPool interface provides you with all the method required to do this:

package gopi

type RPCClientPool interface {
  // Lookup one or more service records
  Lookup(ctx context.Context, name, addr string, max int) ([]RPCServiceRecord, error)

  // Connect to a service instance
  Connect(service RPCServiceRecord, flags RPCFlag) (RPCClientConn, error)

  // Create an RPCClient
  NewClient(string, RPCClientConn) RPCClient
}

For example, here is a function in helloworld-client which creates a new connection:

func Conn(service,addr string,timeout time.Duration) (gopi.RPCClientConn, error) {
  pool := app.ModuleInstance("rpc/clientpool").(gopi.RPCClientPool)
  ctx, cancel := context.WithTimeout(context.Background(),timeout)
  defer cancel()

  if records, err := pool.Lookup(ctx,service,addr, 1); err != nil {
    return nil, err
  } else if len(records) == 0 {
    return nil, gopi.ErrNotFound
  } else if conn, err := pool.Connect(records[0],gopi.RPC_FLAG_NONE); err != nil {
    return nil, err
  } else {
    return conn, nil
  }
}

The Lookup function arguments are:

  • The service name to connect to, or if empty will connect to any remote service instance;
  • The address of the instance including the port. If empty, will connect to any service instance;
  • The maximum number of service records to return, or zero for unlimited.

The Connect function can be used to return an RPCClientConn object from a service record. The second argument can be RPC_FLAG_INET_V4 or RPC_FLAG_INET_V6 if you want to select one protocol or the other:

package gopi

type RPCClientConn interface {
  // Remote address and port
  Addr() string

  // Return list of services published by remote service instance
  Services() ([]string, error)
}

Once you have a client connection, you can create an RPC client, which can then be used to call remote methods. In order to do this, a client module needs to be registered. For example,

package main

import (
  gopi  "github.com/djthorpe/gopi"
  hw    "github.com/djthorpe/gopi-rpc/rpc/grpc/helloworld"
)

////////////////////////////////////////////////////////////////////////////////

func Main(app *gopi.AppInstance, done chan<- struct{}) error {  
  if conn, err := Conn(app); err != nil {
    return err
  } else {
    client_ := pool.NewClient("gopi.Greeter", conn).(*hw.Client)
    if reply, err := client.SayHello(name); err != nil {
      return err
    } else {
      fmt.Println("Service says:",reply)
    }
  }
  return nil
}

////////////////////////////////////////////////////////////////////////////////

func main() {
  // Create the configuration
  config := gopi.NewAppConfig("rpc/helloworld:client")

  // Set flags
  config.AppFlags.FlagString("addr", "localhost:8080", "Gateway address")

  // Run the command line tool
  os.Exit(gopi.CommandLineTool(config, Main))
}

Note that the client module for the helloworld service is called rpc/helloworld:client and in order to register it you need to import the module from github.com/djthorpe/gopi-rpc/rpc/grpc/helloworld.

Writing a service command

A server is a long-running process which can be composed of one or more RPC services. If you have already created the service definitions (which is described in the sections below) you'll simply need to import the service module files and use the gopi.RPCServerTool function to start the server. For example, the following command publishes the gopi.Greeter and gopi.Version services:

package main

import (
  "os"

  // Frameworks
  gopi "github.com/djthorpe/gopi"

  // Modules
  _ "github.com/djthorpe/gopi-rpc/sys/grpc"
  _ "github.com/djthorpe/gopi/sys/logger"

  // Services
  _ "github.com/djthorpe/gopi-rpc/rpc/grpc/helloworld"
  _ "github.com/djthorpe/gopi-rpc/rpc/grpc/version"
)

////////////////////////////////////////////////////////////////////////////////

func main() {
  // Create the configuration
  config := gopi.NewAppConfig("rpc/helloworld:service", "rpc/version:service")

  // Run the server and register all the services
  os.Exit(gopi.RPCServerTool(config))
}

Creating a new service and client

In order to create your own microservice and clients, you'll need to do the following:

  1. Define your service methods and messages in a protocol buffer file;
  2. Generate the gRPC client and service stubs;
  3. Write the module code to interface to the service stub (service.go);
  4. Write the module code to interface to the client stub (client.go);
  5. Write the initialization code to register the modules (init.go);
  6. Optionally, write some serialization logic to translate between native types & protobuf types.

It may seem daunting, and ultimately it's a lot of work and quite a bit of boilerplate. To make it even more daunting, the folder and file structure could look like this, if you were to create a new microservice called foobar for example:

foobar/
  -> foobar.go
  -> rpc/
    -> protobuf/
      -> protobuf.go
      -> foobar/
        -> foobar.proto
    -> gprc/
      -> foobar/
        -> client.go
        -> service.go
        -> init.go
        -> serialize.go
  -> sys/
    -> foobar/
      -> foobar.go
      -> init.go

In short,

  • The foobar/foobar.go file may comtain interface and type definitions when you want to import foobar elsewhere;
  • The foobar/sys/foobar folder contains the module code for your foobar business logic, including the foobar.go driver and the module initialization code init.go;
  • The foobar/rpc/protobuf/foobar/foobar.proto contains your service definition;
  • The foobar/rpc/protobuf/protobuf.go contains a single generate directive to create the client and server stubs;
  • The foobar/grpc/foobar folder contains the client, server and serialization code and also the module initialization code in init.go.

The next few subsections describes what you need to put in all the files.

Generating a protocol buffer file for your service definition

TODO

Generating the stubs

TODO

Writing the service module

TODO

Writing the client module

TODO

The initalization code

TODO

Serialization code

TODO

Documentation

Index

Constants

View Source
const (
	DISCOVERY_SERVICE_QUERY = "_services._dns-sd._udp"
)

Variables

This section is empty.

Functions

func Client added in v1.0.15

func Client(config gopi.AppConfig, timeout time.Duration, task ClientTask) int

func FilterBySubtype added in v1.0.15

func FilterBySubtype(services []gopi.RPCServiceRecord, subtype string) []gopi.RPCServiceRecord

FilterBySubtype returns set of records which match subtype

func HasHostPort added in v1.0.15

func HasHostPort(addr string) bool

HasHostPort returns true if string is of type <hostname>:<port>

func LookupServiceRecords added in v1.0.15

func LookupServiceRecords(app *gopi.AppInstance, addr string, timeout time.Duration) ([]gopi.RPCServiceRecord, error)

LookupServiceRecords returns a remote service records, or nil if not found

func MainTask added in v1.0.11

func MainTask(app *gopi.AppInstance, done chan<- struct{}) error

func ProcessEvent added in v1.0.11

func ProcessEvent(app *gopi.AppInstance, server gopi.RPCServer, discovery gopi.RPCServiceDiscovery, evt gopi.RPCEvent) error

func RegisterTask added in v1.0.11

func RegisterTask(app *gopi.AppInstance, start chan<- struct{}, stop <-chan struct{}) error

func Server added in v1.0.11

func Server(config gopi.AppConfig, background_tasks ...gopi.BackgroundTask) int

func ServerTask added in v1.0.11

func ServerTask(app *gopi.AppInstance, start chan<- struct{}, stop <-chan struct{}) error

Types

type ClientTask added in v1.0.15

type ClientTask func(app *gopi.AppInstance, services []gopi.RPCServiceRecord, done chan<- struct{}) error

type DiscoveryClient added in v1.0.7

type DiscoveryClient interface {
	gopi.RPCClient
	gopi.Publisher

	// Ping the remote service instance
	Ping() error

	// Register a service record in database
	Register(gopi.RPCServiceRecord) error

	// Enumerate service names
	Enumerate(DiscoveryType, time.Duration) ([]string, error)

	// Lookup service instances
	Lookup(string, DiscoveryType, time.Duration) ([]gopi.RPCServiceRecord, error)

	// Stream discovery events. filtering by service name
	StreamEvents(ctx context.Context, service string) error
}

type DiscoveryType added in v1.0.6

type DiscoveryType uint

DiscoveryType is either DNS (using DNS-SD) or DB (using internal database)

const (
	DISCOVERY_TYPE_NONE DiscoveryType = 0
	DISCOVERY_TYPE_DNS  DiscoveryType = 1
	DISCOVERY_TYPE_DB   DiscoveryType = 2
)

type GoogleCast added in v1.0.11

type GoogleCast interface {
	gopi.Driver
	gopi.Publisher

	Devices() []GoogleCastDevice
}

type GoogleCastClient added in v1.0.11

type GoogleCastClient interface {
	gopi.RPCClient
	gopi.Publisher

	// Ping remote service
	Ping() error

	// Return devices from the remote service
	Devices() ([]GoogleCastDevice, error)

	// Stream discovery events
	StreamEvents(ctx context.Context) error
}

type GoogleCastDevice added in v1.0.11

type GoogleCastDevice interface {
	Id() string
	Name() string
	Model() string
	Service() string
	State() uint
}

type GoogleCastEvent added in v1.0.11

type GoogleCastEvent interface {
	gopi.Event

	Type() gopi.RPCEventType
	Device() GoogleCastDevice
	Timestamp() time.Time
}

type GreeterClient added in v1.0.7

type GreeterClient interface {
	gopi.RPCClient

	// Ping the remote service instance
	Ping() error

	// Return a message from the remote service
	SayHello(name string) (string, error)
}

type ServiceRecord

type ServiceRecord interface {
	gopi.RPCServiceRecord

	// Key returns the PTR record for the service record
	Key() string

	// Expired returns true if TTL has been reached
	Expired() bool

	// Source returns the source of the record
	Source() DiscoveryType

	// Set parameters
	SetService(service, subtype string) error
	SetName(name string) error
	SetHostPort(addr string) error
	SetTTL(time.Duration) error
	AppendIP(...net.IP) error
	AppendTXT(...string) error

	// Get DNS answers
	PTR(zone string, ttl time.Duration) *dns.PTR
	SRV(zone string, ttl time.Duration) *dns.SRV
	TXT(zone string, ttl time.Duration) *dns.TXT
	A(zone string, ttl time.Duration) []*dns.A
	AAAA(zone string, ttl time.Duration) []*dns.AAAA

	// Set parameters from DNS answers
	SetPTR(zone string, rr *dns.PTR) error
	SetSRV(zone string, rr *dns.SRV) error
}

type Util added in v1.0.7

type Util interface {
	gopi.Driver

	// NewEvent creates a new event from source, type and service record
	NewEvent(gopi.Driver, gopi.RPCEventType, gopi.RPCServiceRecord) gopi.RPCEvent

	// NewServiceRecord creates an empty service record
	NewServiceRecord(source DiscoveryType) ServiceRecord

	// Read and write array of service records
	Writer(fh io.Writer, records []ServiceRecord, indent bool) error
	Reader(fh io.Reader) ([]ServiceRecord, error)
}

type VersionClient added in v1.0.7

type VersionClient interface {
	gopi.RPCClient

	// Ping the remote service instance
	Ping() error

	// Return version parameters from the remote service
	Version() (map[string]string, error)
}

Directories

Path Synopsis
cmd
helloworld-client
An example RPC Client tool
An example RPC Client tool
helloworld-service
An RPC Server tool, import the services as modules
An RPC Server tool, import the services as modules
rpc
sys

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL