From Zero To Microservice [1]
How to Design API, Server & Client With Ease?
Previous article:
Next article:
What is gRPC (Again)?
gRPC is a modern open source high performance Remote Procedure Call (RPC) framework that can run in any environment. [1]
It lets you:
define the service interface and the structure of the payload messages using Protobuf (Protocol Buffers) in a Protobuf file named e.g. <service>.proto
, then
generate the server and client stubs automatically using <service>.proto
in a variety of languages and platforms, but also
integrate bi-directional streaming and authentication features of HTTP/2
Planning
Let’s start with the following user story:
As an end user, I want to perform read/write operations on person data.
So we want to design a microservice which looks up person by a selected identifier and/or updates person data.
I choose Go as the programming language due to its simplicity and high speed.
Instead of creating HTTP server / client from scratch, I prefer to use gRPC for code generation.
Setup
Setup folder structure (I named it person
):
Initialize Git: git init
Create a Go package: go mod init github.com/<your-grpc>
Install dependencies:
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2
go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28
Save installed programs into PATH: export PATH="$PATH:$(go env GOPATH)/bin"
API Design
Rephrasing User Story: The server / client should be able to perform get / set operations on a DB consisting of personal data.
I set the name of package: person
(see 7, later in the code generation: a Go package person
will be created). The path of Go package should be specified as: <parent>/person
(see 3).
Using the documentation [2], I defined a brand new service “PersonQuery” as follows.
Service definition is what a client sees from the outside. It specifies all methods which can be called remotely using stubs.
Each remote procedure contains exactly one input and one output message. If no input / output is specified, an empty message can be used instead (see 24).
An input / output message can also be a stream message, which means it blocks the procedure and reads / writes data until termination condition reached (see 15, 18 and 21).
A message specifies the payload message which can be used as input / output. Message fields have a type (e.g. string, float) and the order inside message, starting with 1.
Enum is defined similarly as a message type, but its order is also equal to its values. Zero is the default value of an enum (see 42).
If a field is removed from a message in a new version of the service, the reserved field numbers (see 50) should never be reused.
Similarly how aggregation works [3], the messages can extend a message type by including its payload (see 60).
A message can contain also optional fields, which are set to their default value, if not set.
Type-specific default values are: zero, empty string, false, or a null pointer otherwise. The exact implementation of null pointer depends on the language (NULL in C++, None in Python, nil in Golang, …).
Union concept is not widely accepted in all languages: You have to use exactly one of the fields in a message. If that’s the case (see 84), you can use oneof <union> { ... }
to guarantee that exactly one element inside a message is used at once.
If the requirements of a fields are not fully understood, or you have to iterate over its fields, you can use map<<key_type>, <value_type>>
as a new field type. Similarly, map concept is not implemented across all languages, too.
Using repeated
at the beginning of a simple field, an 1-dimensional array of same type can be defined.
Code Generation
Enter the following command in terminal to generate the client and server stub:
protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative <your-stub>/<your-stub>.proto
Here are the instant results:
Implement the service
The API is successfully created, it’s time to implement the server and the client.
server/main.go
:
Line 23: Server is defined as a data structure with server payload and optional fields you wish to use.
Lines 28, 44: Server struct implements methods GetPerson and SetPerson. Since other methods won’t be used, they are implemented as empty functions.
Lines 29, 46: Message type Person
has built-in getter methods for each field x
, named as GetX
.
Line 45: Empty message type is initialized.
Line 79: Initialize listener which will accept client connections on specified TCP port
Lines 89–90: Initialize a new gRPC server, which copies the attributes and methods of PersonQueryServerStruct
Line 93: Accept client connections and perform operations
Implement the client
Now that gRPC server works as expected, we can define the front-end of the application.
It translates the requests made by end user to valid Remote Procedure Calls on the server, as follows:
client/main.go
:
Lines 34–35: Connection to the server is established with transport credentials
Line 51: Command execution takes place (more in the next image)
Line 59: Since json cannot deal with more complex data structures, protojson has to be used instead.
Lines 62, 69: Here are two examples of Remote Procedure Calls. Methods are called remotely from PersonQueryClient and the call is forwarded to PersonQueryServer.
Build & Run the Application
Open a new terminal for server
Build server: go build -o <your-server> ./server
Run server: go run ./server <args>
Then, open a new terminal for client
Run client: go run client/main.go <args>
To be Continued …
Well, the core functionality is done.
In the following article, I dockerize the application and deploy it to a local Kubernetes cluster. Stay tuned!
From Zero to Microservice [2]
How to Dockerize Your App & Deploy using Kubernetes?
yamaceay.medium.com
References
[1] Quick Start (gRPC): https://grpc.io/docs/languages/go/quickstart/
[2] Language Guide (proto3): https://developers.google.com/protocol-buffers/docs/proto3
[3] Inheritance vs. Aggregation: https://stackoverflow.com/questions/269496/inheritance-vs-aggregation