阅读 1491

[译][Part2]使用Go gRPC微服务构建HTTP/REST服务,加入中间件,Kubernetes部署等等

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

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


当前我们的文件目录如下图:

运行命令

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我们将介绍如何在我们的服务当中加入一些中间件(日志打印与跟踪)

感谢收看!🙏🙏🙏🙏

文章分类
后端
文章标签