gRPC helloworld service, RESTful JSON API gateway and swagger UI

概述

本篇博文完整讲述了如果通过 protocol buffers 定义并启动一个 gRPC 服务,然后在 gRPC 服务上提供一个 RESTful JSON API 的反向代理 gateway,最后通过 swagger ui 来提供 RESTful JSON API 的说明,完整代码 helloworld_restful_swagger

Helloworld gRPC Service

参考 gRPC Quick Start for Python

Install gRPC

安装 gRPC

运行命令,

$ python -m pip install grpcio

安装 gRPC 工具箱

Python 的 gRPC 工具箱包括 protol buffer 编译器 protoc 和一些特定插件用于从 .proto 服务定义文件生成 gRPC server 和 client 的代码。

运行命令,

$ python -m pip install grpcio-tools

服务定义文件 helloworld.proto

在 pb 目录下新建文件 helloworld.proto,然后在其中使用 protocol buffers 语法来编写 gRPC 服务定义。文件内容如下:

syntax = "proto3";

package helloworld;

// The greeting service definition.
service Greeter {
    // Sends a greeting
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
    string name = 1;
}

// The response message containing the greetings
message HelloReply {
    string message = 1;
}

protoc 编译生成 server 和 client 类定义

使用下面命令来生成 gRPC 的 server 和 client 类定义代码文件,

$ python -m grpc.tools.protoc -I. --python_out=. --grpc_python_out=. pb/helloworld.proto

命令没有报错的话,将会在 pb 目录下生成两个文件 helloworld_pb2.py 和 helloworld_pb2_grpc.py。

其中文件 helloworld_pb2.py 包含了 HelloRequest 和 HelloReploy 的消息类定义,而文件 helloworld_pb2_grpc.py 提供了 gRPC server 类(GreeterServicer)和 client 类(GreeterStub)定义。

编写具体的 gRPC 服务类

文件 helloworld_pb2_grpc.py 提供了 gRPC server 类(GreeterServicer)提供了 gRPC 服务的规范定义,没有具体的实现。我们需要自己编写 gRPC 服务类文件 server.py,代码如下,

from concurrent import futures
import time

import grpc

import pb.helloworld_pb2 as pb_dot_helloworld__pb2
import pb.helloworld_pb2_grpc as pb_dot_helloworld_pb2__grpc

_ONE_DAY_IN_SECONDS = 60 * 60 * 24

class MyServer(pb_dot_helloworld_pb2__grpc.GreeterServicer):
    def SayHello(self, request, context):
        print("Receive request, request.name: {0}".format(request.name))
        return pb_dot_helloworld__pb2.HelloReply(
            message='Hello, {0}'.format(request.name))


def serve():
    server = grpc.server(futures.ThreadPoolExecutor(max_workers=10))
    pb_dot_helloworld_pb2__grpc.add_GreeterServicer_to_server(MyServer(), server)
    server.add_insecure_port('[::]:50051')
    print("GreeterServicer start at port 50051...")
    server.start()
    try:
        while True:
            time.sleep(_ONE_DAY_IN_SECONDS)
    except KeyboardInterrupt:
        server.stop(0)

if __name__ == '__main__':
    serve() 

然后启动 gRPC server,

$ python server.py
lienhuadeMacBook-Pro:helloworld_restful_swagger lienhua34$ python server.py 
GreeterServicer start at port 50051...

编写 gRPC client.py

文件 helloworld_pb2_grpc.py 提供了 gRPC client 类(GreeterStub)定义。我们需要编写自己的 client.py 代码来通过 GreeterStub 调用 gRPC server 方法。代码内容如下:

import grpc
import pb.helloworld_pb2 as pb_dot_helloworld__pb2
import pb.helloworld_pb2_grpc as pb_dot_helloworld_pb2__grpc

def run():
    channel = grpc.insecure_channel('localhost:50051')
    stub = pb_dot_helloworld_pb2__grpc.GreeterStub(channel)
    response = stub.SayHello(pb_dot_helloworld__pb2.HelloRequest(name="world"))
    print("GreeterService client received: " + response.message)

if __name__ == '__main__':
    run() 

运行 client.py 代码,

lienhuadeMacBook-Pro:helloworld_restful_swagger lienhua34$ python client.py 
GreeterService client received: Hello, world

至此,可见我们的 gRPC helloworld 服务已经可用。

RESTful JSON API Gateway

调用 gRPC 服务需要自己编写相对应的 client 代码才行,这无疑给访问 gRPC 带来了一定的难度。我们可以通过在 gRPC 服务上面提供一个 RESTful API gateway,可以直接通过 RESTful JSON API 来访问。

grpc-gateway 是 protoc 的一个插件,用于读取 gRPC 服务定义,然后生成一个反向代理服务来将 RESTful JSON API 转换为 gRPC 调用。

Install grpc-gateway

确保你本地安装了 golang 6.0 以上版本,并且将 $GOPATH/bin 添加到 $PATH 中。然后运行下面命令,

$ 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
$ go get -u github.com/golang/protobuf/protoc-gen-go

修改 helloworld.proto

修改文件 helloworld.proto,添加gateway option,

syntax = "proto3";

package helloworld;

import "google/api/annotations.proto"

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {
    option (google.api.http) = {
      post: "/v1/hello"
      body: "*"
    };
  }
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

生成 gRPC golang stub 类

运行下面命令生成 gRPC golang stub 类文件,

$ python -m grpc.tools.protoc -I. 
       -I/usr/local/include 
       -I$GOPATH/src 
       -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis 
       --go_out=Mgoogle/api/annotations.proto=github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis/google/api,plugins=grpc:. 
       pb/helloworld.proto

此时便在 pb 目录下生成 helloworld.pb.go 文件。

生成反向代理代码

运行下面命令生成反向代理代码,

$ python -m grpc.tools.protoc -I. 
       -I/usr/local/include 
       -I$GOPATH/src 
       -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis 
       --grpc-gateway_out=logtostderr=true:. 
       pb/helloworld.proto

没有报错的话,将在 pb 目录下生成文件 helloworld.pb.gw.go。

编写 entrypoint 文件

编写 entrypoint 文件 proxy.go,内容如下:

package main

import (
    "flag"
    "log"
    "net/http"

    "github.com/grpc-ecosystem/grpc-gateway/runtime"
    "golang.org/x/net/context"
    "google.golang.org/grpc"

    gw "github.com/lienhua34/notes/grpc/helloworld_restful_swagger/pb"
)

var (
    greeterEndpoint = flag.String("helloworld_endpoint", "localhost:50051", "endpoint of Greeter gRPC Service")
)

func run() error {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    mux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithInsecure()}
    err := gw.RegisterGreeterHandlerFromEndpoint(ctx, mux, *greeterEndpoint, opts)
    if err != nil {
        return err
    }

    log.Print("Greeter gRPC Server gateway start at port 8080...")
    http.ListenAndServe(":8080", mux)
    return nil
}

func main() {
    flag.Parse()

    if err := run(); err != nil {
        log.Fatal(err)
    }
}

编译,生成可执行文件 helloworld_restful_swagger,

$ go build .

启动服务

先启动 gRPC 服务,

lienhuadeMacBook-Pro:helloworld_restful_swagger lienhua34$ python server.py 
GreeterServicer start at port 50051...

然后启动 RESTful JSON API gateway,

lienhuadeMacBook-Pro:helloworld_restful_swagger lienhua34$ ./helloworld_restful_swagger 
2017/01/12 15:59:17 Greeter gRPC Server gateway start at port 8080...

通过 curl 进行访问,

lienhuadeMacBook-Pro:helloworld_restful_swagger lienhua34$ curl -X POST -k http://localhost:8080/v1/hello -d '{"name": "world"}'
{"message":"Hello, world"}
lienhuadeMacBook-Pro:helloworld_restful_swagger lienhua34$ curl -X POST -k http://localhost:8080/v1/hello -d '{"name": "lienhua34"}'
{"message":"Hello, lienhua34"}

自此,RESTful JSON API gateway 已经可用了。

swagger UI

RESTful JSON API 的 Swagger 说明

通过下面命令可以生成 RESTful JSON API 的 swagger 说明文件。

$ python -m grpc.tools.protoc -I. 
       -I/usr/local/include 
       -I$GOPATH/src 
       -I$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis 
       --swagger_out=logtostderr=true:. 
       pb/helloworld.proto

该命令在 pb 目录下生成一个 helloworld.swagger.json 文件。我们在 pb 目录下直接新增一个文件 helloworld.swagger.go,然后在里面定义一个常量 Swagger,内容即为 helloworld.swagger.json 的内容。

修改 proxy.go 文件中的 run() 方法来添加一个 API 路由来返回 swagger.json 的内容,

func run() error {
    ctx := context.Background()
    ctx, cancel := context.WithCancel(ctx)
    defer cancel()

    mux := http.NewServeMux()
    mux.HandleFunc("/swagger.json", func(w http.ResponseWriter, req *http.Request) {
        io.Copy(w, strings.NewReader(gw.Swagger))
    })

    gwmux := runtime.NewServeMux()
    opts := []grpc.DialOption{grpc.WithInsecure()}
    err := gw.RegisterGreeterHandlerFromEndpoint(ctx, gwmux, *greeterEndpoint, opts)
    if err != nil {
        return err
    }

    log.Print("Greeter gRPC Server gateway start at port 8080...")
    http.ListenAndServe(":8080", mux)
    return nil
}

重新编译,并启动 RESTful gateway,然后访问 http://localhost:8080/swagger.json 便得到 helloworld RESTful API 的 swagger 说明了。

但是,swagger.json 内容显示太不直观了。swagger 提供了非常好的可视化 swagger-ui。我们将 swagger-ui 添加到我们的 gateway 中。

下载 swagger-ui 代码

Swagger 提供了可视化的 API 说明。我们可以在 RESTful JSON API gateway 中添加 swagger-ui。

将 Swagger 源码的 dist 目录下包含了 swagger ui 所需的 HTML、css 和 js 代码文件,我们将该目录下的所有文件拷贝到 third_party/swagger-ui 目录下。

将 swagger-ui 文件制作成 go 内置文件

我们可以使用 go-bindata 将 swagger-ui 的文件制作成 go 内置的数据文件进行访问。

先安装 go-bindata,

$ go get -u github.com/jteeuwen/go-bindata/...

然后将 third-party/swagger-ui 下的所有文件制作成 go 内置数据文件,

$ go-bindata --nocompress -pkg swagger -o pkg/ui/data/swagger/datafile.go third_party/swagger-ui/...

生成文件 pkg/ui/data/swagger/datafile.go,

$ ls -l pkg/ui/data/swagger/datafile.go 
-rw-r--r--  1 lienhua34  staff  3912436  1 12 22:56 pkg/ui/data/swagger/datafile.go

swagger-ui 文件服务器

使用 go-bindata 将 swagger-ui 制作成 go 内置数据文件之后,我们便可以使用 elazarl/go-bindata-assetfs 结合 net/http 来将 swagger-ui 内置数据文件对外提供服务。

安装 elazarl/go-bindata-assetfs,

$ go get github.com/elazarl/go-bindata-assetfs/...

然后修改 proxy.go 代码,最终代码请看 proxy.go

重新编译,然后启动 gateway 服务,在浏览器中输入 http://localhost:8080/swagger-ui,

但是上面打开的 swagger-ui 默认打开的是一个 http://petstore.swagger.io/v2/swagger.json 的 API 说明信息。我们需要在输入框中输入我们 API 的地址 http://localhost:8080/swagger.json ,然后点击回车键才能看到我们的 API 说明,

如果我们想让它打开的时候默认就是我们的 API 说明怎么办?将文件 third_party/swagger-ui/index.html 中的 http://petstore.swagger.io/v2/swagger.json 替换成 http://localhost:8080/swagger.json ,然后重新生成 pkg/ui/data/swagger/datafile.go 文件,再重新编译一下即可。

参考:

********************************************************

转载请标注原创出处:lienhua34 http://www.cnblogs.com/lienhua34/p/6285829.html

********************************************************

原文地址:https://www.cnblogs.com/lienhua34/p/6285829.html