[教程,Part 2]如何使用HTTP/REST端点,中间件,Kubernetes等开发Go gRPC微服务

640?wx_fmt=png

这是第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...")
            <-ctx.Done()
        }

        _, 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=<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...

这意味着服务器已启动。打开另一个终端来构建和运行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


640?wx_fmt=png


第五届 Gopher China 大会更多动态:


【重要通知】@所有人,大会报名倒计时11天!欲报从速,错过再等一年!


Gopher China 2019 讲师专访-bilibili架构师毛剑


Gopher China 2019 讲师专访-TutorABC研发总监董海冰

探探Gopher China 2019大会全面启动


最后两周粉丝福利:


福利优惠码:GopherChina


Gopher China  2019大会企业团购通道即将关闭

详情请加微信号:13458572960(玉璧)


戳下方阅读原文即可报名本次 Gopher China 大会!


640?wx_fmt=png

GO 中国征稿啦!


自“Go中国  ” 公众号上线以来,因为扎实的干货(害羞)、前沿的解读(娇羞)、满满的福利一直深受 Gopher 们的喜爱,为了给大家带来更具实力的干货以及 Go 语项目开发经验,我们将开始对外征稿!


现在我们开始对外征稿啦!如果你有优秀的 Go 语言技术文章想要分享,热点的行业资讯需要报道等,欢迎联系在菜单栏回复“投稿”“合作”联系我们的小编进行投稿。‍

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值