歡迎光臨
每天分享高質量文章

[教程,Part 2]如何使用HTTP/REST端點,中間件,Kubernetes等開發Go gRPC微服務

這是第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: "[email protected]";
        };
    };
    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:%[email protected](%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

    已同步到看一看
    赞(0)

    分享創造快樂