3.5. gRPC và Protobuf extensions
Hiện nay, cộng đồng Open source đã phát triển rất nhiều extensions xung quanh Protobuf và gRPC, tạo thành một hệ sinh thái to lớn. Ở phần này sẽ trình bày về một số extensions thông dụng.
3.5.1 Validator
Trong Protobuf chúng ta có thể quy định giá trị mặc định của các trường thông qua phần mở rộng, ví dụ:
hello.proto:
syntax = "proto3";
package main;
// import phần mở rộng của protobuf
import "google/protobuf/descriptor.proto";
// định nghĩa một số trường trong phần mở rộng
extend google.protobuf.FieldOptions {
// những con số như: 50000, 50001 là duy nhất cho mỗi trường
string default_string = 50000;
int32 default_int = 50001;
}
// định nghĩa nội dung message
message Message {
// default_string là giá trị mặc định cho name
string name = 1 [(default_string) = "gopher"];
// tương tự, age sẽ có giá trị 10 nếu không khởi trị
int32 age = 2[(default_int) = 10];
}
Trong cộng đồng Open source, thư viện go-proto-validators là một extension của Protobuf có chức năng validator rất mạnh mẽ dựa trên phần mở rộng tự nhiên của Protobuf. Để sử dụng validator đầu tiên ta cần phải tải plugin sinh mã nguồn bên dưới:
$ go get github.com/mwitkow/go-proto-validators/protoc-gen-govalidators
Sau đó thêm phần validation rules
vào các thành viên của Message dựa trên rules của go-proto-validators validator.
hello.proto: (dùng thư viện validator)
syntax = "proto3";
package main;
// import file validator.proto
import "github.com/mwitkow/go-proto-validators/validator.proto";
// định nghĩa message
message Message {
// dấu ngoặc vuông mang ý nghĩa là phần tùy chọn
string important_string = 1 [
// regex sẽ validate trường important_string đúng theo syntax hay không
(validator.field) = {regex: "^[a-z]{2,5}$"}
];
int32 age = 2 [
// tương tự, giá trị của a sẽ được validate lớn hơn 0 và nhỏ hơn 100
(validator.field) = {int_gt: 0, int_lt: 100}
];
}
Tất cả những validation rules được định nghĩa trong message FieldValidator
trong file validator.proto. Trong đó ta sẽ thấy một số trường được dùng ở ví dụ trên như sau:
mwitkow/go-proto-validators/validator.proto:
syntax = "proto2";
package validator;
import "google/protobuf/descriptor.proto";
extend google.protobuf.FieldOptions {
optional FieldValidator field = 65020;
}
message FieldValidator {
// sử dụng Golang RE2-syntax regex để match với nội dung các field
optional string regex = 1;
// giá trị của biến integer bình thường lớn hơn giá trị này.
optional int64 int_gt = 2;
// giá trị của biến integer bình thường nhỏ hơn giá trị này.
optional int64 int_lt = 3;
// ...
}
Phần chú thích của mỗi trường ở trên sẽ cho chúng ta thông tin về chức năng của chúng. Sau khi chọn được các chức năng validate cần thiết, chúng ta dùng lệnh sau để sinh ra mã nguồn validator:
$ protoc \
--proto_path=${GOPATH}/src \
--proto_path=${GOPATH}/src/github.com/google/protobuf/src \
--proto_path=. \
--govalidators_out=. --go_out=plugins=grpc:.\
hello.proto
// Trong đó:
// - proto_path: đường dẫn đến tất cả các file .proto được sử dụng
// - govalidators_out: plugin sinh ra mã nguồn validator
// Chú ý:
// - Trong Windows, ta thay thế ${GOPATH} thành %GOPATH%
Lệnh trên sẽ gọi chương trình protoc-gen-govalidators
để sinh ra file với tên hello.validator.pb.go
, nội dung của nó sẽ như sau:
hello.validator.pb.go:
// định nghĩa chuỗi regex
var _regex_Message_ImportantString = regexp.MustCompile("^[a-z]{2,5}$")
// hàm Validate() sẽ chạy các rules và bắt lỗi nếu có
func (this *Message) Validate() error {
// rule 1 kiểm tra ImportantString có theo regex hay không, nếu có lỗi sẽ ném ra
if !_regex_Message_ImportantString.MatchString(this.ImportantString) {
return go_proto_validators.FieldError("ImportantString", fmt.Errorf(
`value '%v' must be a string conforming to regex "^[a-z]{2,5}$"`,
this.ImportantString,
))
}
// rule 2 kiểm tra Age > 0 hay không, nếu có lỗi sẽ ném ra
if !(this.Age > 0) {
return go_proto_validators.FieldError("Age", fmt.Errorf(
`value '%v' must be greater than '0'`, this.Age,
))
}
// rule 3 kiểm tra Age < 100 hay không, nếu có lỗi sẽ ném ra
if !(this.Age < 100) {
return go_proto_validators.FieldError("Age", fmt.Errorf(
`value '%v' must be less than '100'`, this.Age,
))
}
// trả về nil nếu kiểm tra tất cả các rules trên đều hợp lệ
return nil
}
Thông qua hàm Validate() được sinh ra, chúng có thể được kết hợp với gRPC interceptor
, chúng ta có thể dễ dàng validate giá trị của tham số đầu vào và kết quả trả về của mỗi hàm.
3.5.2 REST interface
Hiện nay RESTful JSON API vẫn là sự lựa chọn hàng đầu cho các ứng dụng web hay mobile. Vì tính tiện lợi và dễ dùng của RESTful API nên chúng ta vẫn sử dụng nó để frondend có thể giao tiếp với hệ thống backend. Nhưng khi chúng ta sử dụng framework gRPC của Google để xây dựng các service. Các service sử dụng gRPC thì dễ dàng trao đổi dữ liệu với nhau dựa trên giao thức HTTP/2 và protobuf, nhưng ở phía frontend lại sử dụng RESTful API API hoạt động trên giao thức HTTP/1. Vấn đề đặt ra là chúng ta cần phải chuyển đổi các yêu cầu RESTful API thành các yêu cầu gRPC để hệ thống các service gRPC có thể hiểu được.
Cộng đồng Open source đã xây dựng một project với tên gọi là grpc-gateway, nó sẽ sinh ra một proxy có vai trò chuyển các yêu cầu REST HTTP thành các yêu cầu gRPC HTTP2.
gRPC-Gateway workflow
Trong file Protobuf (chỉ có ở proto3), chúng ta sẽ thêm thông tin phần routing ứng với các hàm trong gRPC service, để dựa vào đó grpc-gateway sẽ sinh ra mã nguồn proxy tương ứng.
rest_service.proto:
// phiên bản proto3
syntax = "proto3";
// tên package được sinh ra
package main;
// chú ý: import annotations.proto để dùng chức năng grpc-gateway
import "google/api/annotations.proto";
// định nghĩa message trao đổi
message StringMessage {
string value = 1;
}
// định nghĩa RestService
service RestService {
// định nghĩa hàm RPC Get trong service
rpc Get(StringMessage) returns (StringMessage) {
// nội dung phần option trong này định nghĩa Rest API ra bên ngoài
option (google.api.http) = {
// get: là tên phương thức được sử dụng
get: "/get/{value}"
// "/get/{value}" : là đường dẫn uri,
// trong đó {value} được pass vào uri là nội dung StringMessage request
};
}
// định nghĩa hàm RPC Post trong service
rpc Post(StringMessage) returns (StringMessage) {
option (google.api.http) = {
// dùng phương thức post
post: "/post"
// StringMessage sẽ dưới dạng chuỗi Json khi gửi Request (vd: '{"value":"Hello, World"}')
body: "*"
};
}
}
Chúng ta cài đặt plugin protoc-gen-grpc-gateway với những lệnh sau:
$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
Sau đó chúng ta sinh ra mã nguồn routing cho grpc-gateway thông qua plugin sau:
$ protoc -I/usr/local/include -I. \
-I$GOPATH/src \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--grpc-gateway_out=. --go_out=plugins=grpc:.\
hello.proto
// Trong windows: Thay thế ${GOPATH} với %GOPATH%.
Plugin sẽ sinh ra hàm RegisterRestServiceHandlerFromEndpoint() cho RestService service như sau:
func RegisterRestServiceHandlerFromEndpoint(
ctx context.Context, mux *runtime.ServeMux, endpoint string,
opts []grpc.DialOption,
) (err error) {
...
}
Hàm RegisterRestServiceHandlerFromEndpoint được dùng để chuyển tiếp những request được định nghĩa trong REST interface đến gRPC service. Sau khi registering các Route handle, chúng ta sẽ chạy proxy web service trong hàm main như sau:
proxy/main.go:
func main() {
// khai báo biến context để xử lý signal kết thúc goroutine
ctx := context.Background()
ctx, cancel := context.WithCancel(ctx)
// hàm cancel() sẽ kích hoạt ctx.Done()
defer cancel()
// mux được dùng cho việc routing
mux := runtime.NewServeMux()
// gọi hàm để đăng kí RestService cho proxy
err := RegisterRestServiceHandlerFromEndpoint(
// truyền vào biến ctx, mux, và địa chỉ gRPC service
ctx, mux, "localhost:5000",
[]grpc.DialOption{grpc.WithInsecure()},
)
// in ra lỗi nếu có
if err != nil {
log.Fatal(err)
}
// bắt đầu lắng nghe http client trên port 8080
http.ListenAndServe(":8080", mux)
}
// $ go run proxy/main.go
Tiếp theo ta sẽ chạy gRPC service:
restservice/main.go:
// khai báo struct xây dựng RestService
type RestServiceImpl struct{}
// hàm Get RPC được xây dựng như sau
func (r *RestServiceImpl) Get(ctx context.Context, message *StringMessage) (*StringMessage, error) {
return &StringMessage{Value: "Get hi:" + message.Value + "#"}, nil
}
// tương tự với hàm Post RPC được xây dựng với
func (r *RestServiceImpl) Post(ctx context.Context, message *StringMessage) (*StringMessage, error) {
return &StringMessage{Value: "Post hi:" + message.Value + "@"}, nil
}
// hàm main của gRPC service
func main() {
// khởi tạo một grpc Server mới
grpcServer := grpc.NewServer()
// register grpc Server với đối tượng xây dựng các hàm RPC
RegisterRestServiceServer(grpcServer, new(RestServiceImpl))
// listen gRPC Service trên port 5000, bỏ qua lỗi trả về nếu có
lis, _ := net.Listen("tcp", ":5000")
grpcServer.Serve(lis)
}
// $ go run restservice/main.go
Sau khi chạy hai chương trình gRPC và REST services, chúng ta có thể tạo request REST service với lệnh curl:
// gọi service Get
$ curl localhost:8080/get/gopher
{"value":"Get: gopher"}
// gọi service Post
$ curl localhost:8080/post -X POST --data '{"value":"grpc"}'
{"value":"Post: grpc"}
Khi chúng ta publishing REST interface thông qua Swagger, một swagger file có thể được sinh ra nhờ vào công cụ grpc-gateway bằng lệnh bên dưới:
// chạy lệnh sau để cài đặt nếu chưa có sẵn
$ go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
// lệnh sinh ra swagger file
$ protoc -I. \
-I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--swagger_out=. \
hello.proto
// Trong đó,
// - --swagger_out=.: dùng plugin swagger để sinh ra swagger file tại thư mục hiện tại
File hello.swagger.json
sẽ được sinh ra sau đó. Trong trường hợp này, chúng ta có thể dùng swagger-ui project
để cung cấp tài liệu REST interface
và testing dưới dạng web pages.
3.5.3 Dùng Docker grpc-gateway
Với những lập trình viên phát triển gRPC Services trên các ngôn ngữ không phải Golang như Java, C++, ... có nhu cầu sinh ra grpc gateway cho các services của họ nhưng gặp khá nhiều khó khăn từ việc cài đặt môi trường Golang, Protobuf, các lệnh generate,v,v.. Có một giải pháp đơn giản hơn đó là sử dụng Docker để xây dựng grpc-gateway theo bài hướng dẫn chi tiết sau buildingdocker-grpc-gateway.
3.5.4 Nginx
Những phiên bản Nginx về sau cũng đã hỗ trợ gRPC
với khả năng register nhiều gRPC service instance giúp load balancing (cân bằng tải) dễ dàng hơn. Những extension của Nginx về gRPC là một chủ đề lớn, ở đây chúng tôi không trình bày hết được, các bạn có thể tham khảo các tài liệu trên trang chủ của Nginx như ở đây.
Liên kết
- Phần tiếp theo: Công cụ grpcurl
- Phần trước: Một số vấn đề khác của gRPC
- Mục lục