這是第1部分(part 1)的延續。前一部分的結果是gRPC服務和客戶端。本部分專門介紹如何將HTTP / REST端點新增到gRPC服務。您可以在此處找到第2部分的完整原始碼。
要新增HTTP / REST端點,我們將使用很棒的grpc-gateway庫。有一篇很棒的文章詳細描述了grpc-gateway的工作原理:https://medium.com/@thatcher/why-choose-between-grpc-and-rest-bc0d351f2f84
Step 1:將REST註釋新增到API定義
首先,我們必須安裝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”檔案夾。
我們需要從grpc-gateway包中獲取包含的proto:
將“%GOPATH%/ src / github.com / grpc-ecosystem / grpc-gateway / third_party / googleapis / google”檔案夾的內容複製到專案中的“third_party / google”檔案夾中。
在third_party專案檔案夾中建立“protoc-gen-swagger / options”檔案夾:
mkdir -p third_party/protoc-gen-swagger/options
然後將annotations.proto和openapiv2.proto檔案從“%GOPATH%/ src / github.com / grpc-ecosystem / grpc-gateway / protoc-gen-swagger / options”檔案夾複製到“third_party / protoc-gen-swagger / options” “專案中的檔案夾。
在繼續之前,我們假設已經安裝了Proto編譯器的Go語言程式碼生成器外掛。執行以下命令以確保:
go get -u github.com/golang/protobuf/protoc-gen-go
接下來,我們必須在ToDo服務的api / proto / v1 / todo-service.proto檔案中新增REST註釋(請參閱此處的詳細資訊):
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/fengberlin/go-grpc-http-rest-microservice-tutorial";
email: "fengberlin@qq.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;
}
}
}
}
};
// 用於管理待辦事項串列的服務
service ToDoService {
// 建立新的待辦事項任務
rpc Create (CreateRequest) returns (CreateResponse) {
option (google.api.http) = {
post: "/v1/todo"
body: "*"
};
}
// 讀取待辦事項任務
rpc Read(ReadRequest) returns (ReadResponse) {
option (google.api.http) = {
get: "/v1/todo/{id}"
};
}
// 更新待辦事項任務
rpc Update(UpdateRequest) returns (UpdateResponse) {
option (google.api.http) = {
put: "/v1/todo/{toDo.id}"
body: "*"
additional_bindings {
patch: "/v1/todo/{toDo.id}"
body: "*"
}
};
}
// 刪除待辦事項任務
rpc Delete(DeleteRequest) returns (DeleteResponse) {
option (google.api.http) = {
delete: "/v1/todo/{id}"
};
}
// 讀取全部待辦事項任務
rpc ReadAll(ReadAllRequest) returns (ReadAllResponse) {
option (google.api.http) = {
get: "/v1/todo/all"
};
}
}
// 請求資料以建立新的待辦事項任務
message CreateRequest {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 要新增的任務物體
ToDo toDo = 2;
}
// 我們要做的是Task
message ToDo {
// 待辦事項任務的唯一整數識別符號
int64 id = 1;
// 任務的標題
string title = 2;
// 待辦事項任務的詳細說明
string description = 3;
// 提醒待辦任務的日期和時間
google.protobuf.Timestamp reminder = 4;
}
// 包含建立的待辦事項任務的資料
message CreateResponse {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 已建立任務的ID
int64 id = 2;
}
// 求資料讀取待辦事項任務
message ReadRequest {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 待辦事項任務的唯一整數識別符號
int64 id = 2;
}
// 包含ID請求中指定的待辦事項任務資料
message ReadResponse {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 按ID讀取的任務物體
ToDo toDo = 2;
}
// 請求資料以更新待辦事項任務
message UpdateRequest {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 要更新的任務物體
ToDo toDo = 2;
}
// 包含更新操作的狀態
message UpdateResponse {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 包含已更新的物體數量
// 在成功更新的情況下等於1
int64 updated = 2;
}
// 請求資料刪除待辦事項任務
message DeleteRequest {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 要刪除的待辦事項任務的唯一整數識別符號
int64 id = 2;
}
// 包含刪除操作的狀態
message DeleteResponse {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
// 包含已刪除的物體數量
// 成功刪除時等於1
int64 deleted = 2;
}
// 請求資料以讀取所有待辦事項任務
message ReadAllRequest {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
}
// 包含所有待辦事項任務的串列
message ReadAllResponse {
// API版本控制:這是明確指定版本的最佳實踐
string api = 1;
repeated ToDo toDos = 2;
}
您可以在此處閱讀有關proto檔案中Swagger註釋的更多資訊。
然後在專案的根目錄中建立“api / swagger / v1”檔案夾(生成的swagger檔案的輸出位置):
mkdir -p api/swagger/v1
並透過以下內容替換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
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.sh
它更新“pkg / api / v1 / todo-service.pb.go”檔案並建立兩個新檔案:
- pkg / api / v1 / todo-service.pb.gw.go – REST / HTTP生成的stub
- api / swagger / v1 / todo-service.swagger.json – 生成的Swagger檔案
完成。我們在API定義中添加了REST註釋。
Step2:建立HTTP閘道器啟動
使用以下內容在“pkg / protocol / rest”檔案夾中建立server.go檔案:
package rest
import (
"context"
"github.com/fengberlin/go-grpc-http-rest-microservice-tutorial/pkg/api/v1"
"github.com/grpc-ecosystem/grpc-gateway/runtime"
"google.golang.org/grpc"
"log"
"net/http"
"os"
"os/signal"
"time"
)
// RunServer執行HTTP / REST閘道器
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\n", err)
}
srv := &http.Server;{
Addr: ":" + httpPort,
Handler: mux,
}
// 優雅關閉
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
go func() {
for range c {
// 訊號是CTRL+C
log.Println("shutting down gRPC server...")
}
_, cancel := context.WithTimeout(ctx, 5*time.Second)
defer cancel()
_ = srv.Shutdown(ctx)
}()
log.Println("starting HTTP/REST gateway...")
return srv.ListenAndServe()
}
您必須在現實中為閘道器配置HTTPS。請參閱示例如何執行此操作。
然後更新“pkg / cmd / server.go”檔案以啟動HTTP閘道器:
package cmd
import (
"context"
"database/sql"
"flag"
"fmt"
"github.com/fengberlin/go-grpc-http-rest-microservice-tutorial/pkg/protocol/grpc"
"github.com/fengberlin/go-grpc-http-rest-microservice-tutorial/pkg/protocol/rest"
"github.com/fengberlin/go-grpc-http-rest-microservice-tutorial/pkg/service/v1"
// mysql驅動
_ "github.com/go-sql-driver/mysql"
)
// Config是Server的配置
type Config struct {
// gRPC伺服器啟動引數部分
// GRPCPort是gRPC伺服器監聽的TCP埠
GRPCPort string
// HTTP/REST閘道器啟動引數部分
// HTTPPort是透過HTTP/REST閘道器監聽的TCP埠
HTTPPort string
// 資料庫資料儲存引數部分
// DatestoreDBHost是資料庫的地址
DatastoreDBHost string
// DatastoreDBUser是用於連線資料庫的使用者名稱
DatastoreDBUser string
// DatastoreDBPassword是用於連線資料庫的密碼
DatastoreDBPassword string
// DatastoreDBSchema是資料庫的名稱
DatastoreDBSchema string
}
// RunServer執行gRPC伺服器和HTTP閘道器
func RunServer() error {
ctx := context.Background()
// 獲取配置
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)
}
// 新增MySQL驅動程式特定引數來解析 date/time
// 為另一個資料庫刪除它
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)
// 執行HTTP閘道器
go func() {
_ = rest.RunServer(ctx, cfg.GRPCPort, cfg.HTTPPort)
}()
return grpc.RunServer(ctx, v1API, cfg.GRPCPort)
}
您必須知道HTTP閘道器是gRPC服務的包裝器。我的測試顯示大約1-3毫秒的開銷。
Step 3:建立HTTP / REST客戶端
使用以下內容建立“cmd / client-rest / main.go”檔案:
package main
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"time"
)
func main() {
// 獲取配置
address := flag.String("server", "http://localhost:8080", "HTTP gateway url, e.g. http://localhost:8080")
flag.Parse()
t := time.Now().In(time.UTC)
pfx := t.Format(time.RFC3339Nano)
var body string
// 呼叫Create函式
resp, err := http.Post(*address+"/v1/todo", "application/json", strings.NewReader(fmt.Sprintf(`
{
"api":"v1",
"toDo": {
"title":"title (%s)",
"description":"description (%s)",
"reminder":"%s"
}
}
`, pfx, pfx, pfx)))
if err != nil {
log.Fatalf("failed to call Create method: %v\n", err)
}
bodyBytes, err := ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
body = fmt.Sprintf("failed read Create response body: %v", err)
} else {
body = string(bodyBytes)
}
log.Printf("Create response: Code=%d, Body=%s\n\n", resp.StatusCode, body)
// 解析建立的ToDo的ID
var created struct {
API string `json:"api"`
ID string `json:"id"`
}
err = json.Unmarshal(bodyBytes, &created;)
if err != nil {
log.Fatalf("failed to unmarshal JSON response of Create method: %v", err)
fmt.Println("error:", err)
}
// 呼叫Read
resp, err = http.Get(fmt.Sprintf("%s%s/%s", *address, "v1/todo", created.ID))
if err != nil {
log.Fatalf("failed to call Read method: %v", err)
}
bodyBytes, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
body = fmt.Sprintf("failed to read Read response body: %v", err)
} else {
body = string(bodyBytes)
}
log.Printf("Read response: Code=%d, Body=%s\n\n", resp.StatusCode, body)
// 呼叫Update
req, err := http.NewRequest("PUT", fmt.Sprintf("%s%s/%s", *address, "v1/todo", created.ID),
strings.NewReader(fmt.Sprintf(`
{
"api":"v1",
"toDo": {
"title":"title (%s) + updated",
"description":"description (%s) + updated",
"reminder":"%s"
}
}
`, pfx, pfx, pfx)))
req.Header.Set("Content-Type", "application/json")
resp, err = http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("failed to call Update method: %v", err)
}
bodyBytes, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
body = fmt.Sprintf("failed read Update response body: %v", err)
} else {
body = string(bodyBytes)
}
log.Printf("Update response: Code=%d, Body=%s\n\n", resp.StatusCode, body)
// 呼叫ReadAll
resp, err = http.Get(*address + "/v1/todo/all")
if err != nil {
log.Fatalf("failed to call ReadAll method: %v", err)
}
bodyBytes, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
body = fmt.Sprintf("failed read ReadAll response body: %v", err)
} else {
body = string(bodyBytes)
}
log.Printf("ReadAll response: Code=%d, Body=%s\n\n", resp.StatusCode, body)
// 呼叫Delete
req, err = http.NewRequest("DELETE", fmt.Sprintf("%s%s/%s", *address, "/v1/todo", created.ID), nil)
resp, err = http.DefaultClient.Do(req)
if err != nil {
log.Fatalf("failed to call Delete method: %v", err)
}
bodyBytes, err = ioutil.ReadAll(resp.Body)
resp.Body.Close()
if err != nil {
body = fmt.Sprintf("failed read Delete response body: %v", err)
} else {
body = string(bodyBytes)
}
log.Printf("Delete response: Code=%d, Body=%s\n\n", resp.StatusCode, body)
}
最後一步是確保HTTP / REST閘道器正常工作。
啟動終端以使用HTTP / REST閘道器構建和執行gRPC伺服器(根據您的SQL資料庫伺服器替換引數):
cd cmd/server
go build .
./server -grpc-port=9090 -http-port=8080 -db-host=:3306 -db-user= -db-password= -db-schema=
如果我們看到:
2018/09/15 21:08:21 starting HTTP/REST gateway...
2018/09/09 08:02:16 starting gRPC server...
這意味著伺服器已啟動。開啟另一個終端來構建和執行HTTP / REST客戶端:
cd cmd/client-rest
go build .
./client-rest -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"}
一切工作正常。
第2部分總結
這就是第2部分的全部內容。我們為gRPC服務和HTTP / REST客戶端開發了HTTP / REST閘道器。
第2部分的原始碼可在此處獲得。
第3部分是關於如何向gRPC服務和HTTP / REST端點新增中介軟體(例如,日誌記錄/跟蹤)。 謝謝!
via: https://medium.com/@amsokol.com/tutorial-how-to-develop-go-grpc-microservice-with-http-rest-endpoint-middleware-kubernetes-daebb36a97e9
作者: Aleksandr Sokolovskii
譯者: Berlin
朋友會在“發現-看一看”看到你“在看”的內容