Original text: medium.com/@amsokol.co…
Article (translation required)
I’m not going to repeat it here. I want To walk you through how To develop a simple CRUD “To Do List” microservice using gPRC and HTTP/REST back end interfaces. I’ll show you how to write test cases and incorporate middleware (request IDS and logging and tracing) into the service. We’ll even end up with some examples of how to build and distribute our microservices on Kubernetes.
Division of articles
The tutorial will be divided into four parts:
- Part1 is about how to build gRPC CRUD server and client
- Part2 is about how to add HTTP/REST interfaces to gRPC services
- Part3 is about how to add some middleware (such as logging and tracing) to HTTP/REST interfaces and gRPC services
- Part4 is about how to write a Kubernetes deployment configuration file, join the health check, and how to build and publish a project to a Kubernetes cluster in Google Cloud
preparation
- This article is not intended to be a basic tutorial on Go, so you should already have some experience writing Golang
- You will need to install Go V1.11 or later, and we will use the third-party module features of Go
- You need experience in installing, configuring, and working with SQL databases
API first
What does this sentence mean?
- API definitions must be programming language/communication protocol/network transport independent
- The API definition must be loosely coupled to the logical implementation of the API
- API version
- I should avoid manually synchronizing the CONTENT of the API definition, the API logic implementation, and the API documentation. The scaffolding I need for the API logic implementation and the API documentation are generated automatically from the API definition file
“To Do List” micro service
The “ToDo List” microservice allows you To manage “ToDo” lists. ToDo entries include the following fields/attributes:
- ID (Unique Integer Identifier)
- The Title (text)
- The Description (text)
- Reminder (timestamp)
The ToDo service contains the typical add, delete, change, search and get all items.
Create the gRPC CRUD service
Step1: create API definitions
Part1 Complete code here
Before we start, let’s build the skeleton of a project. Here’s another excellent scaffold template for the Go project
I’m running Windows 10 X64, but I’m guessing that converting my next CMD command to MacOs/Linux BASH shouldn’t be a big problem
Start by creating the folder and initializing the project
mkdir go-grpc-http-rest-microservice-tutorial
cd go-grpc-http-rest-microservice-tutorial
go mod init github.com/<you>/go-grpc-http-rest-microservice-tutorial
Copy the code
Create a file directory in your project as follows
mkdir -p api\proto\v1
Copy the code
V1 here is the version number of our API
API versioning: It is my best practice to name different versions of API code in different folders
The next step is to Create the todo-service.proto file in the v1 folder and add the todo service definition. Let’s start with the Create method:
syntax = "proto3";
package v1;
import "google/protobuf/timestamp.proto";
// Taks we have to do
message ToDo {
// Unique integer identifier of the todo task
int64 id = 1;
// Title of the task
string title = 2;
// Detail description of the todo task
string description = 3;
// Date and time to remind the todo task
google.protobuf.Timestamp reminder = 4;
}
// Request data to create new todo task
message CreateRequest{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Task entity to add
ToDo toDo = 2;
}
// Response that contains data for created todo task
message CreateResponse{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// ID of created task
int64 id = 2;
}
// Service to manage list of todo tasks
service ToDoService {
// Create new todo task
rpc Create(CreateRequest) returns (CreateResponse);
}
Copy the code
See the syntax for writing Proto
As you can see, API definitions are independent of programming language, communication protocol, and network transport, which is an important hallmark of Protobuf
In order to compile proto files we need to install some tools and dependencies
- Download the binary file of the Proto compiler, poke here
- Decompress the installation package to any directory and add environment variables to the bin directory
- Create the third_party folder in the go-grPC-HTTP-rest-MicroService-tutorial
- Copy all Proto compilers insideincludeThe contents of the folder go tothird_partyIn the
- Install the Go language code generator plug-in for the Proto compiler
go get -u github.com/golang/protobuf/protoc-gen-go
Copy the code
- Make sure we run it in the go-grpc-HTTP-rest-microservice-tutorial directory
# Windows:
.\third_party\protoc-gen.cmd
# MasOS/Linux:
./third_party/protoc-gen.sh
Copy the code
A file named todo-service.pb.go will be created under PKG /model/v1
Now let’s drive the rest of the methods into the ToDo service and compile them
syntax = "proto3";
package v1;
import "google/protobuf/timestamp.proto";
// Taks we have to do
message ToDo {
// Unique integer identifier of the todo task
int64 id = 1;
// Title of the task
string title = 2;
// Detail description of the todo task
string description = 3;
// Date and time to remind the todo task
google.protobuf.Timestamp reminder = 4;
}
// Request data to create new todo task
message CreateRequest{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Task entity to add
ToDo toDo = 2;
}
// Contains data of created todo task
message CreateResponse{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// ID of created task
int64 id = 2;
}
// Request data to read todo task
message ReadRequest{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Unique integer identifier of the todo task
int64 id = 2;
}
// Contains todo task data specified in by ID request
message ReadResponse{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Task entity read by ID
ToDo toDo = 2;
}
// Request data to update todo task
message UpdateRequest{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Task entity to update
ToDo toDo = 2;
}
// Contains status of update operation
message UpdateResponse{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Contains number of entities have beed updated
// Equals 1 in case of succesfull update
int64 updated = 2;
}
// Request data to delete todo task
message DeleteRequest{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Unique integer identifier of the todo task to delete
int64 id = 2;
}
// Contains status of delete operation
message DeleteResponse{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// Contains number of entities have beed deleted
// Equals 1 in case of succesfull delete
int64 deleted = 2;
}
// Request data to read all todo task
message ReadAllRequest{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
}
// Contains list of all todo tasks
message ReadAllResponse{
// API versioning: it is my best practice to specify version explicitly
string api = 1;
// List of all todo tasks
repeated ToDo toDos = 2;
}
// Service to manage list of todo tasks
service ToDoService {
// Create new todo task
rpc Create(CreateRequest) returns (CreateResponse);
// Read todo task
rpc Read(ReadRequest) returns (ReadResponse);
// Update todo task
rpc Update(UpdateRequest) returns (UpdateResponse);
// Delete todo task
rpc Delete(DeleteRequest) returns (DeleteResponse);
// Read all todo tasks
rpc ReadAll(ReadAllRequest) returns (ReadAllResponse);
}
Copy the code
Run the following command again to update the Go code
# Windows:
.\third_party\protoc-gen.cmd
# MasOS/Linux:
./third_party/protoc-gen.sh
Copy the code
At this point, the definition of the API is complete
Step2: Go to implement the API logic
I use MySQL on Google Cloud as the database for persistent storage. You can use any other SQL database you like.
MySQL script to create ToDo table
CREATE TABLE `ToDo` (
`ID` bigint(20) NOT NULL AUTO_INCREMENT,
`Title` varchar(200) DEFAULT NULL.`Description` varchar(1024) DEFAULT NULL.`Reminder` timestamp NULL DEFAULT NULL,
PRIMARY KEY (`ID`),
UNIQUE KEY `ID_UNIQUE` (`ID`));Copy the code
Will I skip the steps of how to install and configure SQL databases and create tables
Create file PKG /service/v1/todo-service.go and the following
package v1
import (
"context"
"database/sql"
"fmt"
"time"
"github.com/golang/protobuf/ptypes"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/api/v1"
)
const (
// apiVersion is version of API is provided by server
apiVersion = "v1"
)
// toDoServiceServer is implementation of v1.ToDoServiceServer proto interface
type toDoServiceServer struct {
db *sql.DB
}
// NewToDoServiceServer creates ToDo service
func NewToDoServiceServer(db *sql.DB) v1.ToDoServiceServer {
return &toDoServiceServer{db: db}
}
// checkAPI checks if the API version requested by client is supported by server
func (s *toDoServiceServer) checkAPI(api string) error {
// API version is "" means use current version of the service
if len(api) > 0 {
ifapiVersion ! = api {return status.Errorf(codes.Unimplemented,
"unsupported API version: service implements API version '%s', but asked for '%s'", apiVersion, api)
}
}
return nil
}
// connect returns SQL database connection from the pool
func (s *toDoServiceServer) connect(ctx context.Context) (*sql.Conn, error) {
c, err := s.db.Conn(ctx)
iferr ! =nil {
return nil, status.Error(codes.Unknown, "failed to connect to database-> "+err.Error())
}
return c, nil
}
// Create new todo task
func (s *toDoServiceServer) Create(ctx context.Context, req *v1.CreateRequest) (*v1.CreateResponse, error) {
// check if the API version requested by client is supported by server
iferr := s.checkAPI(req.Api); err ! =nil {
return nil, err
}
// get SQL connection from pool
c, err := s.connect(ctx)
iferr ! =nil {
return nil, err
}
defer c.Close()
reminder, err := ptypes.Timestamp(req.ToDo.Reminder)
iferr ! =nil {
return nil, status.Error(codes.InvalidArgument, "reminder field has invalid format-> "+err.Error())
}
// insert ToDo entity data
res, err := c.ExecContext(ctx, "INSERT INTO ToDo(`Title`, `Description`, `Reminder`) VALUES(? ,? ,?) ",
req.ToDo.Title, req.ToDo.Description, reminder)
iferr ! =nil {
return nil, status.Error(codes.Unknown, "failed to insert into ToDo-> "+err.Error())
}
// get ID of creates ToDo
id, err := res.LastInsertId()
iferr ! =nil {
return nil, status.Error(codes.Unknown, "failed to retrieve id for created ToDo-> "+err.Error())
}
return &v1.CreateResponse{
Api: apiVersion,
Id: id,
}, nil
}
// Read todo task
func (s *toDoServiceServer) Read(ctx context.Context, req *v1.ReadRequest) (*v1.ReadResponse, error) {
// check if the API version requested by client is supported by server
iferr := s.checkAPI(req.Api); err ! =nil {
return nil, err
}
// get SQL connection from pool
c, err := s.connect(ctx)
iferr ! =nil {
return nil, err
}
defer c.Close()
// query ToDo by ID
rows, err := c.QueryContext(ctx, "SELECT `ID`, `Title`, `Description`, `Reminder` FROM ToDo WHERE `ID`=?",
req.Id)
iferr ! =nil {
return nil, status.Error(codes.Unknown, "failed to select from ToDo-> "+err.Error())
}
defer rows.Close()
if! rows.Next() {iferr := rows.Err(); err ! =nil {
return nil, status.Error(codes.Unknown, "failed to retrieve data from ToDo-> "+err.Error())
}
return nil, status.Error(codes.NotFound, fmt.Sprintf("ToDo with ID='%d' is not found",
req.Id))
}
// get ToDo data
var td v1.ToDo
var reminder time.Time
iferr := rows.Scan(&td.Id, &td.Title, &td.Description, &reminder); err ! =nil {
return nil, status.Error(codes.Unknown, "failed to retrieve field values from ToDo row-> "+err.Error())
}
td.Reminder, err = ptypes.TimestampProto(reminder)
iferr ! =nil {
return nil, status.Error(codes.Unknown, "reminder field has invalid format-> "+err.Error())
}
if rows.Next() {
return nil, status.Error(codes.Unknown, fmt.Sprintf("found multiple ToDo rows with ID='%d'",
req.Id))
}
return &v1.ReadResponse{
Api: apiVersion,
ToDo: &td,
}, nil
}
// Update todo task
func (s *toDoServiceServer) Update(ctx context.Context, req *v1.UpdateRequest) (*v1.UpdateResponse, error) {
// check if the API version requested by client is supported by server
iferr := s.checkAPI(req.Api); err ! =nil {
return nil, err
}
// get SQL connection from pool
c, err := s.connect(ctx)
iferr ! =nil {
return nil, err
}
defer c.Close()
reminder, err := ptypes.Timestamp(req.ToDo.Reminder)
iferr ! =nil {
return nil, status.Error(codes.InvalidArgument, "reminder field has invalid format-> "+err.Error())
}
// update ToDo
res, err := c.ExecContext(ctx, "UPDATE ToDo SET `Title`=? , `Description`=? , `Reminder`=? WHERE `ID`=?",
req.ToDo.Title, req.ToDo.Description, reminder, req.ToDo.Id)
iferr ! =nil {
return nil, status.Error(codes.Unknown, "failed to update ToDo-> "+err.Error())
}
rows, err := res.RowsAffected()
iferr ! =nil {
return nil, status.Error(codes.Unknown, "failed to retrieve rows affected value-> "+err.Error())
}
if rows == 0 {
return nil, status.Error(codes.NotFound, fmt.Sprintf("ToDo with ID='%d' is not found",
req.ToDo.Id))
}
return &v1.UpdateResponse{
Api: apiVersion,
Updated: rows,
}, nil
}
// Delete todo task
func (s *toDoServiceServer) Delete(ctx context.Context, req *v1.DeleteRequest) (*v1.DeleteResponse, error) {
// check if the API version requested by client is supported by server
iferr := s.checkAPI(req.Api); err ! =nil {
return nil, err
}
// get SQL connection from pool
c, err := s.connect(ctx)
iferr ! =nil {
return nil, err
}
defer c.Close()
// delete ToDo
res, err := c.ExecContext(ctx, "DELETE FROM ToDo WHERE `ID`=?", req.Id)
iferr ! =nil {
return nil, status.Error(codes.Unknown, "failed to delete ToDo-> "+err.Error())
}
rows, err := res.RowsAffected()
iferr ! =nil {
return nil, status.Error(codes.Unknown, "failed to retrieve rows affected value-> "+err.Error())
}
if rows == 0 {
return nil, status.Error(codes.NotFound, fmt.Sprintf("ToDo with ID='%d' is not found",
req.Id))
}
return &v1.DeleteResponse{
Api: apiVersion,
Deleted: rows,
}, nil
}
// Read all todo tasks
func (s *toDoServiceServer) ReadAll(ctx context.Context, req *v1.ReadAllRequest) (*v1.ReadAllResponse, error) {
// check if the API version requested by client is supported by server
iferr := s.checkAPI(req.Api); err ! =nil {
return nil, err
}
// get SQL connection from pool
c, err := s.connect(ctx)
iferr ! =nil {
return nil, err
}
defer c.Close()
// get ToDo list
rows, err := c.QueryContext(ctx, "SELECT `ID`, `Title`, `Description`, `Reminder` FROM ToDo")
iferr ! =nil {
return nil, status.Error(codes.Unknown, "failed to select from ToDo-> "+err.Error())
}
defer rows.Close()
var reminder time.Time
list := []*v1.ToDo{}
for rows.Next() {
td := new(v1.ToDo)
iferr := rows.Scan(&td.Id, &td.Title, &td.Description, &reminder); err ! =nil {
return nil, status.Error(codes.Unknown, "failed to retrieve field values from ToDo row-> "+err.Error())
}
td.Reminder, err = ptypes.TimestampProto(reminder)
iferr ! =nil {
return nil, status.Error(codes.Unknown, "reminder field has invalid format-> "+err.Error())
}
list = append(list, td)
}
iferr := rows.Err(); err ! =nil {
return nil, status.Error(codes.Unknown, "failed to retrieve data from ToDo-> "+err.Error())
}
return &v1.ReadAllResponse{
Api: apiVersion,
ToDos: list,
}, nil
}
Copy the code
Create file PKG /service/v1/todo-service.go for API logic
Step3: write test cases for the API logic implementation
We should write test cases for whatever we develop. This is a mandatory rule.
There is a great mock library for testing interactive GO-SQL Mock for SQL databases. I’ll use it to write test cases for our ToDo service.
Place this file in the PKG /service/v1 directory and the current project file structure is as follows
Step4: Write gRPC server
Create a file PKG/protocol/GRPC/server. Go and write
package grpc
import (
"context"
"log"
"net"
"os"
"os/signal"
"google.golang.org/grpc"
"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/api/v1"
)
// RunServer runs gRPC service to publish ToDo service
func RunServer(ctx context.Context, v1API v1.ToDoServiceServer, port string) error {
listen, err := net.Listen("tcp".":"+port)
iferr ! =nil {
return err
}
// register service
server := grpc.NewServer()
v1.RegisterToDoServiceServer(server, v1API)
// graceful shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func(a) {
for range c {
// sig is a ^C, handle it
log.Println("shutting down gRPC server...")
server.GracefulStop()
<-ctx.Done()
}
}()
// start gRPC server
log.Println("starting gRPC server...")
return server.Serve(listen)
}
Copy the code
The RunServer function is responsible for registering the ToDo service and starting the gRPC service
You need to configure TLS for gPRC service. See examples to learn how to configure TLS
Then create PKG/CMD /server/server.go and the corresponding contents
package cmd
import (
"context"
"database/sql"
"flag"
"fmt"
// mysql driver
_ "github.com/go-sql-driver/mysql"
"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/protocol/grpc"
"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/service/v1"
)
// Config is configuration for Server
type Config struct {
// gRPC server start parameters section
// gRPC is TCP port to listen by gRPC server
GRPCPort string
// DB Datastore parameters section
// DatastoreDBHost is host of database
DatastoreDBHost string
// DatastoreDBUser is username to connect to database
DatastoreDBUser string
// DatastoreDBPassword password to connect to database
DatastoreDBPassword string
// DatastoreDBSchema is schema of database
DatastoreDBSchema string
}
// RunServer runs gRPC server and HTTP gateway
func RunServer(a) error {
ctx := context.Background()
// get configuration
var cfg Config
flag.StringVar(&cfg.GRPCPort, "grpc-port".""."gRPC port to bind")
flag.StringVar(&cfg.DatastoreDBHost, "db-host".""."Database host")
flag.StringVar(&cfg.DatastoreDBUser, "db-user".""."Database user")
flag.StringVar(&cfg.DatastoreDBPassword, "db-password".""."Database password")
flag.StringVar(&cfg.DatastoreDBSchema, "db-schema".""."Database schema")
flag.Parse()
if len(cfg.GRPCPort) == 0 {
return fmt.Errorf("invalid TCP port for gRPC server: '%s'", cfg.GRPCPort)
}
// add MySQL driver specific parameter to parse date/time
// Drop it for another database
param := "parseTime=true"
dsn := fmt.Sprintf("%s:%s@tcp(%s)/%s? %s",
cfg.DatastoreDBUser,
cfg.DatastoreDBPassword,
cfg.DatastoreDBHost,
cfg.DatastoreDBSchema,
param)
db, err := sql.Open("mysql", dsn)
iferr ! =nil {
return fmt.Errorf("failed to open database: %v", err)
}
defer db.Close()
v1API := v1.NewToDoServiceServer(db)
return grpc.RunServer(ctx, v1API, cfg.GRPCPort)
}
Copy the code
The RunServer function is responsible for reading the parameters entered from the command line, creating the database connection, creating the ToDo service instance, and calling the RunServer function in the previous gPRC service
Finally create the following file CMD /server/main.go and the contents
package main
import (
"fmt"
"os"
"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/cmd"
)
func main(a) {
iferr := cmd.RunServer(); err ! =nil {
fmt.Fprintf(os.Stderr, "%v\n", err)
os.Exit(1)}}Copy the code
That’s all the code for the server. The current project directory is as follows
Step5: create a gRPC client
Create the file CMD /client-grpc/main.go and the following
package main
import (
"context"
"flag"
"log"
"time"
"github.com/golang/protobuf/ptypes"
"google.golang.org/grpc"
"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/api/v1"
)
const (
// apiVersion is version of API is provided by server
apiVersion = "v1"
)
func main(a) {
// get configuration
address := flag.String("server".""."gRPC server in format host:port")
flag.Parse()
// Set up a connection to the server.
conn, err := grpc.Dial(*address, grpc.WithInsecure())
iferr ! =nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
c := v1.NewToDoServiceClient(conn)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
t := time.Now().In(time.UTC)
reminder, _ := ptypes.TimestampProto(t)
pfx := t.Format(time.RFC3339Nano)
// Call Create
req1 := v1.CreateRequest{
Api: apiVersion,
ToDo: &v1.ToDo{
Title: "title (" + pfx + ")",
Description: "description (" + pfx + ")",
Reminder: reminder,
},
}
res1, err := c.Create(ctx, &req1)
iferr ! =nil {
log.Fatalf("Create failed: %v", err)
}
log.Printf("Create result: <%+v>\n\n", res1)
id := res1.Id
// Read
req2 := v1.ReadRequest{
Api: apiVersion,
Id: id,
}
res2, err := c.Read(ctx, &req2)
iferr ! =nil {
log.Fatalf("Read failed: %v", err)
}
log.Printf("Read result: <%+v>\n\n", res2)
// Update
req3 := v1.UpdateRequest{
Api: apiVersion,
ToDo: &v1.ToDo{
Id: res2.ToDo.Id,
Title: res2.ToDo.Title,
Description: res2.ToDo.Description + " + updated",
Reminder: res2.ToDo.Reminder,
},
}
res3, err := c.Update(ctx, &req3)
iferr ! =nil {
log.Fatalf("Update failed: %v", err)
}
log.Printf("Update result: <%+v>\n\n", res3)
// Call ReadAll
req4 := v1.ReadAllRequest{
Api: apiVersion,
}
res4, err := c.ReadAll(ctx, &req4)
iferr ! =nil {
log.Fatalf("ReadAll failed: %v", err)
}
log.Printf("ReadAll result: <%+v>\n\n", res4)
// Delete
req5 := v1.DeleteRequest{
Api: apiVersion,
Id: id,
}
res5, err := c.Delete(ctx, &req5)
iferr ! =nil {
log.Fatalf("Delete failed: %v", err)
}
log.Printf("Delete result: <%+v>\n\n", res5)
}
Copy the code
The above is all the client code, the current project directory is as follows
Step6: start the client and server of gRPC
The final step is to make sure the gPRC service runs
Start a terminal build and run gRPC service (replace the following database connection parameters with your own database configuration)
cd cmd/server
go build .
server.exe -grpc-port=9090 -db-host=<HOST>:3306 -db-user=<USER> -db-password=<PASSWORD> -db-schema=<SCHEMA>
Copy the code
If I could see
2018/09/09 08:02:16 starting gRPC server...
Copy the code
Proof that our service is up and running
Open the build and Run gRPC clients of the other terminal
cd cmd/client-grpc
go build .
client-grpc.exe -server=localhost:9090
Copy the code
If you can see the following information:
2018/09/09 09:16:01 Create result: <api:"v1" id:13 >
2018/09/09 09:16:01 Read result: <api:"v1" toDo:<id:13 title:"The title (the 2018-09-09 T06:16:01. 5755011 z)" description:"The description (the 2018-09-09 T06:16:01. 5755011 z)" reminder:<seconds:1536473762 > > >
2018/09/09 09:16:01 Update result: <api:"v1" updated:1 >
2018/09/09 09:16:01 ReadAll result: <api:"v1" toDos:<id:9 title:"The title (the 2018-09-09 T04: they shall. 3693282 z)" description:"The description (the 2018-09-09 T04: they shall. 3693282 z)" reminder:<seconds:1536468316 > > toDos:<id:10 title:"The title (the 2018-09-09 T04:46:00. 7490565 z)" description:"The description (the 2018-09-09 T04:46:00. 7490565 z)" reminder:<seconds:1536468362 > > toDos:<id:13 title:"The title (the 2018-09-09 T06:16:01. 5755011 z)" description:"The description (the 2018-09-09 T06:16:01. 5755011 z) plus updated." reminder:<seconds:1536473762 > > >
2018/09/09 09:16:01 Delete result: <api:"v1" deleted:1 >
Copy the code
Everything is working fine!
That’s all for Part1. We successfully built the client and server of gRPC
The source code for Part1 is here
Part2 describes how to add HTTP/REST interfaces to the gRPC service we established in this chapter.
Thanks for watching! 🙏 🙏 🙏 🙏