[譯][Part2]使用Go gRPC微服務構建HTTP/REST服務,加入中介軟體,Kubernetes部署等等

野生程式元發表於2019-10-24

原文:medium.com/@amsokol.co…

[譯][Part2]使用Go gRPC微服務構建HTTP/REST服務,加入中介軟體,Kubernetes部署等等

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.protoopenapiv2.proto檔案複製到我們專案中的third_party\protoc-gen-swagger/options


當前我們的檔案目錄如下圖:

[譯][Part2]使用Go gRPC微服務構建HTTP/REST服務,加入中介軟體,Kubernetes部署等等

執行命令

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文件生成

當前我們的專案結構如下:

[譯][Part2]使用Go gRPC微服務構建HTTP/REST服務,加入中介軟體,Kubernetes部署等等

以上就是將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毫秒的開銷。


當前目錄結構如下:

[譯][Part2]使用Go gRPC微服務構建HTTP/REST服務,加入中介軟體,Kubernetes部署等等


Step3: 建立HTTP/REST客戶端

建立cmd/client-rest/main.go檔案以及以下內容,戳我

當前目錄結構如下:

[譯][Part2]使用Go gRPC微服務構建HTTP/REST服務,加入中介軟體,Kubernetes部署等等

最後一步來確保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我們將介紹如何在我們的服務當中加入一些中介軟體(日誌列印與跟蹤)

感謝收看!????

相關文章