在Part1中我們已經構建了一個gPRC的服務端以及客戶端,本章介紹如何在gRPC服務端中加入HTTP/REST介面提供服務。完整的part2程式碼戳這裡
為了加入HEEP/REST介面我打算用一個非常好的庫grpc-gateway。下面有一篇很棒的文章來介紹更多關於grpc-gateway的工作原理。
Setp1: 往API定義檔案中加入REST註解
首先我們安裝grpc-gateway以及swagger文件生成器外掛
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-grpc-gateway
go get -u github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger
複製程式碼
grpc-gateway會被安裝在GOPATH/src/github.com/grpc-ecosystem/grpc-gateway檔案下。我們需要將裡面的third_party/googleapis/google檔案拷貝到我們目錄的third_party/google下,並且建立protoc-gen-swagger/options資料夾在third_party資料夾內
mkdir -p third_party\protoc-gen-swagger\options
複製程式碼
然後將GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/protoc-gen-swagger/options中的annotations.proto和openapiv2.proto檔案複製到我們專案中的third_party\protoc-gen-swagger/options下
當前我們的檔案目錄如下圖:
執行命令
go get -u github.com/golang/protobuf/protoc-gen-go
複製程式碼
接下來將REST的註解檔案引入到api/proto/v1/todo-service.proto中
syntax = "proto3";
package v1;
import "google/protobuf/timestamp.proto";
import "google/api/annotations.proto";
import "protoc-gen-swagger/options/annotations.proto";
option (grpc.gateway.protoc_gen_swagger.options.openapiv2_swagger) = {
info: {
title: "ToDo service";
version: "1.0";
contact: {
name: "go-grpc-http-rest-microservice-tutorial project";
url: "https://github.com/amsokol/go-grpc-http-rest-microservice-tutorial";
email: "medium@amsokol.com";
};
};
schemes: HTTP;
consumes: "application/json";
produces: "application/json";
responses: {
key: "404";
value: {
description: "Returned when the resource does not exist.";
schema: {
json_schema: {
type: STRING;
}
}
}
}
};
// Task 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 {
// Read all todo tasks
rpc ReadAll(ReadAllRequest) returns (ReadAllResponse){
option (google.api.http) = {
get: "/v1/todo/all"
};
}
// Create new todo task
rpc Create(CreateRequest) returns (CreateResponse){
option (google.api.http) = {
post: "/v1/todo"
body: "*"
};
}
// Read todo task
rpc Read(ReadRequest) returns (ReadResponse){
option (google.api.http) = {
get: "/v1/todo/{id}"
};
}
// Update todo task
rpc Update(UpdateRequest) returns (UpdateResponse){
option (google.api.http) = {
put: "/v1/todo/{toDo.id}"
body: "*"
additional_bindings {
patch: "/v1/todo/{toDo.id}"
body: "*"
}
};
}
// Delete todo task
rpc Delete(DeleteRequest) returns (DeleteResponse){
option (google.api.http) = {
delete: "/v1/todo/{id}"
};
}
}
複製程式碼
你可以點這裡檢視更多的Swagger註解檔案在proto檔案內的用法
建立api/swagger/v1檔案
mkdir -p api\swagger\v1
複製程式碼
通過以下命令更新third_party/protoc-gen.cmd檔案的內容
protoc --proto_path=api/proto/v1 --proto_path=third_party --go_out=plugins=grpc:pkg/api/v1 todo-service.proto
protoc --proto_path=api/proto/v1 --proto_path=third_party --grpc-gateway_out=logtostderr=true:pkg/api/v1 todo-service.proto
protoc --proto_path=api/proto/v1 --proto_path=third_party --swagger_out=logtostderr=true:api/swagger/v1 todo-service.proto
複製程式碼
進入go-grpc-http-rest-microservice-tutorial檔案執行以下命令
.\third_party\protoc-gen.cmd
複製程式碼
它會更新pkg/api/v1/todo-service.pb.go檔案以及建立兩個新的檔案:
- pkg\api\v1\todo-service.pb.gw.go -- REST/HTTP骨架生成
- api\swagger\v1\todo-service.swagger.json -- swagger文件生成
當前我們的專案結構如下:
以上就是將REST註解加入API定義檔案的步驟
Step2: 建立HTTP啟動閘道器
建立server.go檔案在pkg/protocol/rest下以及以下內容
package rest
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"time"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"google.golang.org/grpc"
"github.com/amsokol/go-grpc-http-rest-microservice-tutorial/pkg/api/v1"
)
// RunServer runs HTTP/REST gateway
func RunServer(ctx context.Context, grpcPort, httpPort string) error {
ctx, cancel := context.WithCancel(ctx)
defer cancel()
mux := runtime.NewServeMux()
opts := []grpc.DialOption{grpc.WithInsecure()}
if err := v1.RegisterToDoServiceHandlerFromEndpoint(ctx, mux, "localhost:"+grpcPort, opts); err != nil {
log.Fatalf("failed to start HTTP gateway: %v", err)
}
srv := &http.Server{
Addr: ":" + httpPort,
Handler: mux,
}
// graceful shutdown
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
// sig is a ^C, handle it
}
_, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
}()
log.Println("starting HTTP/REST gateway...")
return srv.ListenAndServe()
}
複製程式碼
真實場景你需要配置HTPPS閘道器,例子參考這裡
接下來更新pkg/cmd/server.go檔案去開啟HTTP閘道器
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/protocol/rest"
"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
// GRPCPort is TCP port to listen by gRPC server
GRPCPort string
// HTTP/REST gateway start parameters section
// HTTPPort is TCP port to listen by HTTP/REST gateway
HTTPPort 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.HTTPPort, "http-port", "", "HTTP 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)
}
if len(cfg.HTTPPort) == 0 {
return fmt.Errorf("invalid TCP port for HTTP gateway: '%s'", cfg.HTTPPort)
}
// 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)
// run HTTP gateway
go func() {
_ = rest.RunServer(ctx, cfg.GRPCPort, cfg.HTTPPort)
}()
return grpc.RunServer(ctx, v1API, cfg.GRPCPort)
}
複製程式碼
你需要清楚的一點是HTTP閘道器是對gRPC服務的一個封裝。我的測試顯示會增加1-3毫秒的開銷。
當前目錄結構如下:
Step3: 建立HTTP/REST客戶端
建立cmd/client-rest/main.go檔案以及以下內容,戳我
當前目錄結構如下:
最後一步來確保HTTP/REST閘道器能正常工作:
開啟終端build和run HTTP/REST閘道器的gRPC服務
cd cmd/server
go build .
server.exe -grpc-port=9090 -http-port=8080 -db-host=<HOST>:3306 -db-user=<USER> -db-password=<PASSWORD> -db-schema=<SCHEMA>
複製程式碼
如果看到
2018/09/15 21:08:21 starting HTTP/REST gateway...
2018/09/09 08:02:16 starting gRPC server...
複製程式碼
意味這我們的服務已經正常啟動了,這時開啟另一個終端build以及run HTTP/REST客戶端
cd cmd/client-rest
go build .
client-rest.exe -server=http://localhost:8080
複製程式碼
如果看到輸出
2018/09/15 21:10:05 Create response: Code=200, Body={"api":"v1","id":"24"}
2018/09/15 21:10:05 Read response: Code=200, Body={"api":"v1","toDo":{"id":"24","title":"title (2018-09-15T18:10:05.3600923Z)","description":"description (2018-09-15T18:10:05.3600923Z)","reminder":"2018-09-15T18:10:05Z"}}
2018/09/15 21:10:05 Update response: Code=200, Body={"api":"v1","updated":"1"}
2018/09/15 21:10:05 ReadAll response: Code=200, Body={"api":"v1","toDos":[{"id":"24","title":"title (2018-09-15T18:10:05.3600923Z) + updated","description":"description (2018-09-15T18:10:05.3600923Z) + updated","reminder":"2018-09-15T18:10:05Z"}]
}
2018/09/15 21:10:05 Delete response: Code=200, Body={"api":"v1","deleted":"1"}
複製程式碼
所有東西都生效了!
最後
這就是Part2所有的介紹,在本章我們在gRPC的服務端上建立了HTTP/REST服務,所有的程式碼可以檢視此處
接下來Part3我們將介紹如何在我們的服務當中加入一些中介軟體(日誌列印與跟蹤)
感謝收看!????