Hẳn chúng ta ai cũng quen làm việc với các REST API. Tuy nhiên, trong môi trường microservice, việc sử dụng REST API để giao tiếp giữa các service sẽ gây ra độ trễ đáng kể. gRPC ra đời để giải quyết vấn đề này. Trong blog này mình sẽ trình bày nội dung cơ bản liên quan đến gRPC và làm một todo list app demo để chúng ta biết cách sử dụng gRPC trong thực tế nhé.

1. gRPC là gì

gRPC là một RPC platform được phát triển bởi Google nhằm tối ưu hoá và tăng tốc việc giao tiếp giữa các service với nhau trong kiến trúc microservice.

gRPC dùng Protocal Buffer giảm kích thước request và response data, RPC để đơn giản hoá trong việc tạo ra các giao tiếp giữa các service với nhau, HTTP/2 để tăng tốc gửi/nhận HTTP request.

Để hiểu rõ hơn về gRPC bạn có thể tham khảo link sau: Dùng lý thuyết củ hành để tìm hiểu gRPC

Có thể hiểu nôm na gRPC là tương tự như REST dùng để giao tiếp giữa các service, tuy nhiên tốc độ gRPC nhanh hơn REST rất nhiều, bù lại gRPC khó sử dụng và rườm rà hơn. Bạn chỉ nên sử dụng gRPC khi có vấn đề về độ trễ trong việc giao tiếp giữa các service trong kiến trúc microservice.

Chúng ta sẽ bắt đầu làm một todo list app để biết gRPC sử dụng ra sao nhé.

2. Yêu cầu:

Để hiểu nội dung blog này bạn cần phải có những kiến thức sau:

  • Golang (ver 1.11 trở lên để sử dụng Go module)
  • SQL cơ bản (ở đây mình dùng v8, nếu dùng thấp hơn bạn hãy tự convert sql của mình sang sql thích hợp).
  • Web client mình sử dụng beego nên cần phải biết chút ít về Beego.

3. Tạo gRPC CRUD service

Trong phần này mình sẽ tạo một gRPC CRUD service (gRPC server) và một test-client để test nó.

Phần này mình tham khảo một bài blog trên medium, bạn có thể truy cập link bài viết gốc:
How to develop Go gRPC microservice with HTTP/REST endpoint, middleware, Kubernetes deployment, etc.

Các hướng dẫn sau là dành cho môi trường windows sử dụng git bash, nếu bạn dùng linux hoặc mac thì hãy tự convert sang lệnh tương tự.

Định nghĩa api

Đầu tiên tại một nơi ngoài GOPATH, tạo thư mục project và dùng Go module để init.

mkdir todo-grpc
cd todo-grpc
go mod init github.com/hieuvecto/todo-grpc

Tạo folder cho phần định nghĩa api

mkdir -p api/proto/v1

Tiếp theo tạo file todo-service.proto bên trong thư mục api/proto/v1 và thêm các định nghĩa của các phương thức CRUD:

// todo-service.proto
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;

    google.protobuf.Timestamp insert_at = 4;

    google.protobuf.Timestamp update_at = 5;
}

// 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);
}

Về chi tiết cú pháp và ngôn ngữ của .proto, bạn có thể tham khảo bài viết sau: Chi tiết cú pháp proto

Để compile file Proto, chúng ta cần phải cài một số tool và package cần thiết sau.

  • Tải proto compiler binary ở đây: Protocolbuffers/Protobuf (Lưu ý là tải file platform-x64-.zip nhé)
  • Giải nén file zip đến bất kì thư mục trên máy và thêm đường dẫn thư mục "bin" trong thư mục đã giải nén vào biến PATH môi trường windows.
  • Tạo thư mục "third_party" trong thư mục root của project (todo-grpc)
  • Copy tất cả từ thư mục "include" của Proto compiler đến thư mục "third_party"

Cài đặt code generator plugin của golang cho Proto compiler.

go get -u github.com/golang/protobuf/protoc-gen-go 

Tạo file protoc-gen.sh trong thư mục "third_party":

// protoc-gen.sh
protoc --proto_path=api/proto/v1 --proto_path=third_party --go_out=plugins=grpc:pkg/api/v1 todo-service.proto

Tạo thư mục ouput cho file go được generate từ file .proto

mkdir -p pkg/api/v1

Đảm bảo lệnh sau được chạy trong thư mục root (todo-grpc) của project để compile file .proto

sh ./third_party/protoc-gen.sh   

Nó sẽ tạo ra (generate) file go từ file .proto và nằm trong thư mục "pkg/model/v1":
Cấu trúc của file sẽ như sau:

uc?id=10kef8a2UsPCsKBIR-62kkDEU_GDT5b3C&export=download

Triển khai api

Tiếp theo chúng ta sẽ triển khai grpc api bằng golang như sau:
Chúng ta cần một database để lưu trữ data của todo, ở đây mình dùng mysql.
Tạo bảng ToDo với cú pháp sau:

CREATE TABLE `ToDo` (
  `ID` int(6) NOT NULL AUTO_INCREMENT,
  `Title` varchar(200) NOT NULL,
  `Description` varchar(1024) DEFAULT NULL,
  `InsertAt` timestamp NOT NULL,
  `UpdateAt` timestamp NOT NULL,
  PRIMARY KEY (`ID`),
  UNIQUE KEY `ID_UNIQUE` (`ID`)
);

Tạo file "pkg/service/v1/todo-service.go" với nội dung sau:

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/hieuvecto/todo-grpc/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 {
		if apiVersion != 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)
	if err != 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
	if err := s.checkAPI(req.Api); err != nil {
		return nil, err
	}

	// get SQL connection from pool
	c, err := s.connect(ctx)
	if err != nil {
		return nil, err
	}
	defer c.Close()

	insert_at, err := ptypes.Timestamp(req.ToDo.InsertAt)
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, "insert_at field has invalid format-> "+err.Error())
	}

	update_at, err := ptypes.Timestamp(req.ToDo.UpdateAt)
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, "update_at field has invalid format-> "+err.Error())
	}

	// insert ToDo entity data
	res, err := c.ExecContext(ctx, "INSERT INTO ToDo(`Title`, `Description`, `InsertAt`, `UpdateAt`) VALUES(?, ?, ?, ?)",
		req.ToDo.Title, req.ToDo.Description, insert_at, update_at)
	if err != nil {
		return nil, status.Error(codes.Unknown, "failed to insert into ToDo-> "+err.Error())
	}

	// get ID of creates ToDo
	id, err := res.LastInsertId()
	if err != 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
	if err := s.checkAPI(req.Api); err != nil {
		return nil, err
	}

	// get SQL connection from pool
	c, err := s.connect(ctx)
	if err != nil {
		return nil, err
	}
	defer c.Close()

	// query ToDo by ID
	rows, err := c.QueryContext(ctx, "SELECT * FROM ToDo WHERE `ID`=?",
		req.Id)
	if err != nil {
		return nil, status.Error(codes.Unknown, "failed to select from ToDo-> "+err.Error())
	}
	defer rows.Close()

	if !rows.Next() {
		if err := 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 insert_at time.Time
	var update_at time.Time
	if err := rows.Scan(&td.Id, &td.Title, &td.Description, &insert_at, &update_at); err != nil {
		return nil, status.Error(codes.Unknown, "failed to retrieve field values from ToDo row-> "+err.Error())
	}
	td.InsertAt, err = ptypes.TimestampProto(insert_at)
	if err != nil {
		return nil, status.Error(codes.Unknown, "insert_at field has invalid format-> "+err.Error())
	}
	td.UpdateAt, err = ptypes.TimestampProto(update_at)
	if err != nil {
		return nil, status.Error(codes.Unknown, "update_at 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
	if err := s.checkAPI(req.Api); err != nil {
		return nil, err
	}

	// get SQL connection from pool
	c, err := s.connect(ctx)
	if err != nil {
		return nil, err
	}
	defer c.Close()

	update_at, err := ptypes.Timestamp(req.ToDo.UpdateAt)
	if err != nil {
		return nil, status.Error(codes.InvalidArgument, "update_at field has invalid format-> "+err.Error())
	}

	// update ToDo
	res, err := c.ExecContext(ctx, "UPDATE ToDo SET `Title`=?, `Description`=?, `UpdateAt`=? WHERE `ID`=?",
		req.ToDo.Title, req.ToDo.Description, update_at, req.ToDo.Id)
	if err != nil {
		return nil, status.Error(codes.Unknown, "failed to update ToDo-> "+err.Error())
	}

	rows, err := res.RowsAffected()
	if err != 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
	if err := s.checkAPI(req.Api); err != nil {
		return nil, err
	}

	// get SQL connection from pool
	c, err := s.connect(ctx)
	if err != nil {
		return nil, err
	}
	defer c.Close()

	// delete ToDo
	res, err := c.ExecContext(ctx, "DELETE FROM ToDo WHERE `ID`=?", req.Id)
	if err != nil {
		return nil, status.Error(codes.Unknown, "failed to delete ToDo-> "+err.Error())
	}

	rows, err := res.RowsAffected()
	if err != 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
	if err := s.checkAPI(req.Api); err != nil {
		return nil, err
	}

	// get SQL connection from pool
	c, err := s.connect(ctx)
	if err != nil {
		return nil, err
	}
	defer c.Close()

	// get ToDo list
	rows, err := c.QueryContext(ctx, "SELECT * FROM ToDo")
	if err != nil {
		return nil, status.Error(codes.Unknown, "failed to select from ToDo-> "+err.Error())
	}
	defer rows.Close()

	var insert_at time.Time
	var update_at time.Time
	list := []*v1.ToDo{}
	for rows.Next() {
		td := new(v1.ToDo)
		if err := rows.Scan(&td.Id, &td.Title, &td.Description, &insert_at, &update_at); err != nil {
			return nil, status.Error(codes.Unknown, "failed to retrieve field values from ToDo row-> "+err.Error())
		}
		td.InsertAt, err = ptypes.TimestampProto(insert_at)
		if err != nil {
			return nil, status.Error(codes.Unknown, "insert_at field has invalid format-> "+err.Error())
		}
		td.UpdateAt, err = ptypes.TimestampProto(update_at)
		if err != nil {
			return nil, status.Error(codes.Unknown, "update_at field has invalid format-> "+err.Error())
		}
		list = append(list, td)
	}

	if err := 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
}    

Vậy là đã xong phần triển khai api

Tạo và khởi động gRPC server

Tạo file "pkg/protocol/grpc/server.go" với nội dung sau:

    
package grpc

import (
	"context"
	"log"
	"net"
	"os"
	"os/signal"

	"google.golang.org/grpc"

	"github.com/hieuvecto/todo-grpc/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)
	if err != 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() {
		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)
}
                         

Hàm RunServer sẽ register ToDo service và khởi động gRPC server.

Tiếp theo tạo file "pkg/cmd/server/server.go" với nội dung sau:

package cmd

import (
	"context"
	"database/sql"
	"flag"
	"fmt"

	// mysql driver
	_ "github.com/go-sql-driver/mysql"

	"github.com/hieuvecto/todo-grpc/pkg/protocol/grpc"
	"github.com/hieuvecto/todo-grpc/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() 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)
	if err != nil {
		return fmt.Errorf("failed to open database: %v", err)
	}
	defer db.Close()

	v1API := v1.NewToDoServiceServer(db)

	return grpc.RunServer(ctx, v1API, cfg.GRPCPort)
}

Hàm RunServer sẽ đọc các tham số từ command line, tạo SQL database connection pool, tạo ToDo service instance và gọi hàm RunServer trước đó của gRPC server.

Cuối cùng thì tạo file "cmd/server/main.go" với các nội dung sau:

package main

import (
	"fmt"
	"os"

	"github.com/hieuvecto/todo-grpc/pkg/cmd"
)

func main() {
	if err := cmd.RunServer(); err != nil {
		fmt.Fprintf(os.Stderr, "%v\n", err)
		os.Exit(1)
	}
}

Vậy là xong phần server. Cấu trúc thư mục sẽ như sau:

uc?id=1HZ0Jz1mr-sezUqIXHEDqQU_BdyDL_QsA&export=download

Tạo test-client để test gRPC server thử coi có chạy ok ko

Tạo file "cmd/test-client/main.go" với nội dung sau:

package main

import (
	"context"
	"flag"
	"log"
	"time"

	"github.com/golang/protobuf/ptypes"
	"google.golang.org/grpc"

	"github.com/hieuvecto/todo-grpc/pkg/api/v1"
)

const (
	// apiVersion is version of API is provided by server
	apiVersion = "v1"
)

func main() {
	// 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())
	if err != 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)
	insert_at, _ := ptypes.TimestampProto(t)
	pfx := t.Format(time.RFC3339Nano)

	// Call Create
	req1 := v1.CreateRequest{
		Api: apiVersion,
		ToDo: &v1.ToDo{
			Title:       "title (" + pfx + ")",
			Description: "description (" + pfx + ")",
			InsertAt:    insert_at,
			UpdateAt: insert_at,
		},
	}
	res1, err := c.Create(ctx, &req1)
	if err != 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)
	if err != 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",
			UpdateAt:    res2.ToDo.UpdateAt,
		},
	}
	res3, err := c.Update(ctx, &req3)
	if err != 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)
	if err != 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)
	if err != nil {
		log.Fatalf("Delete failed: %v", err)
	}
	log.Printf("Delete result: <%+v>\n\n", res5)
}

Chạy thử gRPC server dùng test-client để test

Ta sẽ đảm bảo rằng gRPC server sẽ hoạt động bình thường.
Mở terminal mới để build và run gRPC server (thay thế các tham số để phù hợp với môi trường của bạn):

cd cmd/server
go build .
server.exe -grpc-port=9090 -db-host=<HOST>:3306 -db-user=<USER> -db-password=<PASSWORD> -db-schema=<SCHEMA>

Hiện tại code trên chưa bắn lỗi khi ko kết nối được với database nên nó sẽ luôn luôn chạy nếu build được :))

Mở terminal khác để build và run phần test-client:

cd cmd/test-client
go build .
test-client.exe -server=localhost:9090

Nếu ko gặp lỗi nào, ta sẽ thấy như thế này

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:"title (2018-09-09T06:16:01.5755011Z)" description:"description (2018-09-09T06:16:01.5755011Z)" 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:"title (2018-09-09T04:45:16.3693282Z)" description:"description (2018-09-09T04:45:16.3693282Z)" reminder:<seconds:1536468316 > > toDos:<id:10 title:"title (2018-09-09T04:46:00.7490565Z)" description:"description (2018-09-09T04:46:00.7490565Z)" reminder:<seconds:1536468362 > > toDos:<id:13 title:"title (2018-09-09T06:16:01.5755011Z)" description:"description (2018-09-09T06:16:01.5755011Z) + updated" reminder:<seconds:1536473762 > > >
2018/09/09 09:16:01 Delete result: <api:"v1" deleted:1 >

Tức là gRPC hoạt động bình thường.

4. Tiếp theo chúng ta sẽ tạo web client cho todo list app.

Phần này chỉ dùng beego để tạo 1 web app cơ bản thôi, khi cần lấy data thì web client sẽ gọi đến gRPC server được viết tương tự như phần test-client. Mình sẽ để mẫu một hàm controller như sau:

func (c *TodoController) ReadTodo() {

	address := beego.AppConfig.String("grpc-server")
	apiVersion := beego.AppConfig.String("apiVersion")
	id := c.Ctx.Input.Param(":id")

	id_int, err := strconv.ParseInt(id, 10, 64)
	if err != nil {
		c.Abort("500")
	}

	// Set up a connection to the server.
	conn, err := grpc.Dial(address, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("cannot connect to grpc server: %v", err)
		c.Abort("500")
	}
	defer conn.Close()

	client := v1.NewToDoServiceClient(conn)

	ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
	defer cancel()

	req := v1.ReadRequest{
		Api: apiVersion,
		Id:  id_int,
	}
	res, err := client.Read(ctx, &req)
	if err != nil {
		log.Fatalf("Read failed: %v", err)
		c.Abort("500")
	}
	log.Printf("Read result: <%+v>\n\n", res)

	c.Data["ToDo"] = res.ToDo

	c.TplName = "read.tpl"
}
    

Các bạn chỉ việc dùng thư viện mở rồi đóng kết nối gRPC khi sử dụng xong như bình thường.

Phần này thì các bạn cần phải biết Beego thì mới hiểu được, nên mình sẽ đưa toàn bộ source code để các bạn xem. Code đơn giản thôi nên khi bạn biết beego sẽ dễ dàng hiểu được code.
https://github.com/hieuvecto/todo-grpc

Hoặc nếu các bạn muốn triển khai nhanh mà ko cần phải cài đặt gì (chỉ yêu cầu cài đặt duy nhất vagrant và virtual box), mình có làm sẵn phần deployment cho project này:
https://github.com/hieuvecto/todo-grpc-deployment

Đây là giao diện todo list đơn giản
uc?id=1jXBFq9W3lfEQPVijvZu3h-Scgzdvx7NE&export=download

5. Tổng hợp liên kết:

Dùng lý thuyết củ hành để tìm hiểu gRPC
How to develop Go gRPC microservice with HTTP/REST endpoint, middleware, Kubernetes deployment, etc.
Chi tiết cú pháp proto
Source code todo-grpc
Deployment source code của todo-grpc

6. Tham khảo:

How to develop Go gRPC microservice with HTTP/REST endpoint, middleware, Kubernetes deployment, etc.