gRPC golang开发实践

gRPC golang开发实践

Protobuf

gRPC使用protobuf,首先使用protobuf定义服务,然后使用这个文件来生成客户端和服务端的代码。因为pb是跨语言的,因此即使服务端和客户端语言并不一致也是可以互相序列化和反序列化的。

首先介绍protobuf怎么写。protobuf最新版本为proto3,在这里你可以看到详细的文档说明:Language Guide (proto 3)

定义消息类型

protobuf里最基本的类型就是message,每一个message都会有一个或者多个字段(field),其中字段包含如下元素

  • 类型:类型不仅可以是标量类型(int、string等),也可以是复合类型(enum等),也可以是其他message
  • 字段名:字段名比较推荐的是使用下划线/分隔名称
  • 字段编号:一个message内每一个字段编号都必须唯一的,在编码后其实传递的是这个编号而不是字段名
  • 字段标签 :消息字段可以是以下字段之一
    • optional:表示字段不是必需的,也就是说,在序列化的数据中可以不包含这个字段。字段在消息中是可有可无的。如果字段有值,就序列化这个值;如果没有值,就不序列化这个字段。从 Protobuf 版本 3 开始,optional 关键字不再使用,因为所有字段默认都是可选的。字段可以有默认值,如果没有明确赋值,就会使用这个默认值。
    • repeated:此字段类型可以重复零次或多次,也就是数组。系统会保留重复值的顺序。
    • map:这是一个成对的键值对字段
  • 保留字段:为了避免再次使用到已移除的字段可以设定保留字段。如果任何未来用户尝试使用这些字段标识符,协议缓冲区编译器就会报错

标量类型

标量类型会涉及到不同语言和编码方式,以下是部分proto type和Go type的对应关系。

proto Type Go Type Notes
double float64
float float32
int32 int32 使用可变长度的编码。对负数的编码效率低下 - 如果您的字段可能包含负值,请改用 sint32。
uint32 uint32 使用可变长度的编码。
sint32 int32 使用可变长度的编码。有符号整数值。与常规 int32 相比,这些函数可以更高效地对负数进行编码。
fixed32 uint32 始终为 4 个字节。如果值通常大于 2^28,则比 uint32 更高效。
bool bool
string string 字符串必须始终包含 UTF-8 编码或 7 位 ASCII 文本,并且长度不得超过 232.
bytes []byte 可以包含任意长度的 2^32 字节。

复合类型

  • 数组
bash 复制代码
message SearchResponse {
  repeated Result results = 1;
}

message Result {
  string url = 1;
  string title = 2;
  repeated string snippets = 3;
}
  • 枚举
bash 复制代码
message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}
  • 服务
    定义的method仅能有一个入参和出参数。如果需要传递多个参数需要定义成message
bash 复制代码
service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

引用其它消息类型

使用import引用另外一个文件的pb

bash 复制代码
syntax = "proto3";

import "google/protobuf/wrappers.proto";

package ecommerce;

message Order {
  string id = 1;
  repeated string items = 2;
  string description = 3;
  float price = 4;
  google.protobuf.StringValue destination = 5;
}

Protoc使用

protoc就是protobuf的编译器,它把proto文件编译成不同的语言

安装

安装教程, 参见 Protocol Buffer Compiler Installation. 我们采用 Install pre-compiled binaries (any OS) 的方式,从github 版本发布的资源包中获取protoc可执行文件。

使用

bash 复制代码
$ protoc --help
Usage: protoc [OPTION] PROTO_FILES

  -IPATH, --proto_path=PATH   指定搜索路径
  --plugin=EXECUTABLE:
  
  ....
 
  --cpp_out=OUT_DIR           Generate C++ header and source.
  --csharp_out=OUT_DIR        Generate C# source file.
  --java_out=OUT_DIR          Generate Java source file.
  --js_out=OUT_DIR            Generate JavaScript source.
  --objc_out=OUT_DIR          Generate Objective C header and source.
  --php_out=OUT_DIR           Generate PHP source file.
  --python_out=OUT_DIR        Generate Python source file.
  --ruby_out=OUT_DIR          Generate Ruby source file
  
   @<filename>                proto文件的具体位置
  • 搜索路径参数

    第一个比较重要的参数就是搜索路径参数,即上述展示的-IPATH, --proto_path=PATH。它表示的是我们要在哪个路径下搜索.proto文件,这个参数既可以用-I指定,也可以使用--proto_path=指定。

    如果不指定该参数,则默认在当前路径下进行搜索;另外,该参数也可以指定多次,这也意味着我们可以指定多个路径进行搜索。

  • 语言插件参数

    语言参数即上述的--cpp_out=,--python_out=等,protoc支持的语言长达13种,且都是比较常见的

    运行help出现的语言参数,说明protoc本身已经内置该语言对应的编译插件,我们无需安装。

Language
C++ (include C++ runtime and protoc)
Java
Python
Objective-C
C#
Ruby
PHP

下面的语言是由google维护,通过protoc的插件机制来实现,所以仓库单独维护

Language
Dart
Go
  • proto文件位置参数
    proto文件位置参数即上述的@参数,指定了我们proto文件的具体位置,如proto/user.proto。

语言插件

  • golang插件
    非内置的语言支持就得自己单独安装语言插件,比如--go_out=对应的是protoc-gen-go,安装命令如下:
bash 复制代码
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@v1.28

可以使用下面的命令来生成代码

bash 复制代码
$ protoc --go_out=gen --go_opt=paths=source_relative ./proto/user.proto
bash 复制代码
$ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2

执行code gen命令

bash 复制代码
$ protoc --go-grpc_out=gen --go-grpc_opt=paths=source_relative ./proto/user.proto

以上两个命令会产生如下文件

  • xx.pb.go, protoc-gen-go的产出物,包含所有类型的序列化和反序列化代码
  • xx_grpc.pb.go, protoc-gen-go-grpc的产出物,包含
    • 定义在XX service中的用来给client调用的接口定义
    • 定义在 XX service中的用来给服务端实现的接口定义

Buf使用

可以看到使用protoc的时候,将遇到两个问题:

  1. 随着使用的插件逐渐增多,插件参数变多,命令行执行并不是很方便和直观。例如后面还将使用grpc-gateway+swagger插件。
bash 复制代码
$ protoc --go_out=. --go_opt=paths=source_relative --go-grpc_out=. --go-grpc_opt=paths=source_relative --grpc-gateway_out=logtostderr=true:. ./proto/user.proto
  1. 依赖某些外部的protobuf文件时,只能通过拷贝到本地的方式,也不够方便.
bash 复制代码
import "google/api/annotations.proto";

因此诞生了 Buf 这个项目,它除了能解决上述问题,还有额外的功能

  • 破坏性修改(不兼容性)检查
  • linter
  • 集中式的版本管理

配置和构建buf模块

在pb文件的根目录执行buf config init,初始化buf配置文件。

bash 复制代码
$ buf config init

此时会在根目录多出一个buf.yaml文件,内容为

yaml 复制代码
# buf.yaml
version: v1
breaking:
  use:
    - FILE
lint:
  use:
    - DEFAULT

我们需要显式的指定输入的buf模块(buf模块表示一组被配置、构建和版本控制的Protobuf 文件)。将.proto 文件的子目录路径,添加在buf.yaml 文件modules 字段下

yaml 复制代码
# buf.yaml
version: v2
+modules:
+  - path: proto
lint:
  use:
    - DEFAULT
breaking:
  use:
    - FILE

如果proto文件中依赖第三方的protobuf文件,例如google/type/datetime.proto,可以将依赖添加到buf.yaml 文件的 deps 字段下. 这些protobuf文件是托管在BSR仓库中的模块,例如这里对应的BSR仓库地址是buf.build/googleapis/googleapis

yaml 复制代码
# buf.yaml
version: v2
modules:
  - path: proto
lint:
  use:
    - DEFAULT
breaking:
  use:
    - FILE
+deps:
+  - buf.build/googleapis/googleapis

修改完依赖之后,记得执行命令buf dep update,把你所有的 deps 更新到最新版。此命令会生成 buf.lock 来固定版本。

buf.yaml 文件修改完之后,执行buf build命令将protobuf文件构建为buf镜像。如果没有报错,证明一切顺利,Buf模块设置正确。

代码生成

  • 插件配置
    创建一个buf.gen.yaml ,它是buf生成代码的配置。buf.gen.yaml相比protoc更加直观。
yaml 复制代码
# buf.gen.yaml
version: v2
managed:
  enabled: true
plugins:
  - remote: buf.build/protocolbuffers/go
    out: gen/go
    opt:
      - paths=source_relative  
  - remote: buf.build/grpc/go
    out: gen/go
    opt:
      - paths=source_relative
  - remote: buf.build/grpc-ecosystem/gateway:v2.21.0
    # execute 'go get github.com/grpc-ecosystem/grpc-gateway/v2@v2.21.0' before using this plugin.
    out: gen/go
    opt:
      - paths=source_relative
  - remote: buf.build/grpc-ecosystem/openapiv2:v2.21.0
    out: gen/openapiv2
inputs:
  - directory: proto
  • 生成代码
bash 复制代码
$ buf generate

buf generate 命令将会

  • 搜索每一个buf.yaml配置里的所有protobuf文件
  • 复制所有protobuf文件到内存
  • 编译所有protobuf文件
  • 执行模版文件里的每一个插件

按以上示例,运行命令之后,就和直接使用protoc命令产出的golang代码一样。

该示例使用了4个插件:go插件、grpc go插件、grpc-gateway插件、swagger插件, 对应的生成代码包含go结构体与pb序列化代码、grpc客户端/服务端stub代码、http 反向代理grpc服务的代码、swagger文档。

实现gRPC API

我们创建一个golang项目,实现RPC服务的客户端和服务端。

初始化go.mod文件

如果你没有处在一个golang模块项目下,在编写golang代码之前,请首先初始化一个项目, 例如

bash 复制代码
$ go mod init github.com/bufbuild/buf-tour

实现服务端代码

创建一个main.go文件

bash 复制代码
$ mkdir server
$ touch server/main.go

服务端示例代码如下(来源:examples/helloworld/greeter_server/main.go

go 复制代码
// Package main implements a server for Greeter service.
package main

import (
        "context"
        "flag"
        "fmt"
        "log"
        "net"

        "google.golang.org/grpc"
        pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

var (
        port = flag.Int("port", 50051, "The server port")
)

// server is used to implement helloworld.GreeterServer.
type server struct {
        pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
        log.Printf("Received: %v", in.GetName())
        return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
        flag.Parse()
        lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
        if err != nil {
                log.Fatalf("failed to listen: %v", err)
        }
        s := grpc.NewServer()
        pb.RegisterGreeterServer(s, &server{})
        log.Printf("server listening at %v", lis.Addr())
        if err := s.Serve(lis); err != nil {
                log.Fatalf("failed to serve: %v", err)
        }
}

在以上代码中,pb.UnimplementedGreeterServer就是自动生成的grpc服务对象,它包含proto文件里定义的所有服务方法的stub实现。我们对服务方法进行重写,用真实的实现替换stub实现即可。

构建并运行grpc服务

bash 复制代码
$ go mod tidy
$ go run server/main.go

实现客户端代码

创建一个main.go文件

bash 复制代码
$ mkdir client
$ touch client/main.go

客户端主函数代码示例如下

go 复制代码
// Package main implements a client for Greeter service.
package main

import (
        "context"
        "flag"
        "log"
        "time"

        "google.golang.org/grpc"
        "google.golang.org/grpc/credentials/insecure"
        pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
        defaultName = "world"
)

var (
        addr = flag.String("addr", "localhost:50051", "the address to connect to")
        name = flag.String("name", defaultName, "Name to greet")
)

func main() {
        flag.Parse()
        // Set up a connection to the server.
        conn, err := grpc.NewClient(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("did not connect: %v", err)
        }
        defer conn.Close()
        c := pb.NewGreeterClient(conn)

        // Contact the server and print out its response.
        ctx, cancel := context.WithTimeout(context.Background(), time.Second)
        defer cancel()
        r, err := c.SayHello(ctx, &pb.HelloRequest{Name: *name})
        if err != nil {
                log.Fatalf("could not greet: %v", err)
        }
        log.Printf("Greeting: %s", r.GetMessage())
}

运行客户端代码,调用SayHello方法。

bash 复制代码
$ go run client/main.go

测试gRPC API

测试我们开发好的gRPC API接口,除了编写特定语言的客户端代码进行调用之外,还可以使用一些通用性的工具

使用bloomRPC客户端工具

该工具导入proto 接口文件之后,便可生成方法列表和请求模板。

如果proto文件中有import本地依赖模块,添加proto会失败,先将依赖的proto添加至导入路径

参考:解决导入路径的问题

使用grpcurl命令行工具

grpcurl是一个命令行工具,使用它可以在命令行中访问gRPC服务,就像使用curl访问http服务一样。

注意(注册reflection服务)

该方法要求先在gRPC服务中启用reflection服务

gRPC服务是使用Protobuf(PB)协议的,而PB提供了在运行时获取Proto定义信息的反射功能。grpc-go中的"google.golang.org/grpc/reflection"包就对这个反射功能提供了支持。

在grpc服务端代码,添加两行代码即可

go 复制代码
// Package main implements a server for Greeter service.
package main

import (
        ...
        "google.golang.org/grpc/reflection"
)

...

func main() {
        flag.Parse()
        lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
        if err != nil {
                log.Fatalf("failed to listen: %v", err)
        }
        s := grpc.NewServer()
        
        // 注册grpcurl所需的reflection服务
        reflection.Register(s)
        
        pb.RegisterGreeterServer(s, &server{})
        log.Printf("server listening at %v", lis.Addr())
        if err := s.Serve(lis); err != nil {
                log.Fatalf("failed to serve: %v", err)
        }
}

grpcurl 功能和使用示例

  • 查看服务列表:
bash 复制代码
$ grpcurl -plaintext 127.0.0.1:8080 list
grpc.reflection.v1alpha.ServerReflection
proto.Greeter
  • 查看某个服务的方法列表:
bash 复制代码
$ grpcurl -plaintext 127.0.0.1:8080 list proto.Greeter
proto.Greeter.SayHello
  • 查看方法定义:
bash 复制代码
$ grpcurl -plaintext 127.0.0.1:8080 describe proto.Greeter.SayHello
proto.Greeter.SayHello is a method:
rpc SayHello ( .proto.HelloRequest ) returns ( .proto.HelloReply );
  • 查看请求参数:
bash 复制代码
$ grpcurl -plaintext 127.0.0.1:8080 describe proto.HelloRequest
proto.HelloRequest is a message:
message HelloRequest {
  string name = 1;
}
  • 调用服务,参数传json即可:
bash 复制代码
$ grpcurl -d '{"name": "abc"}' -plaintext 127.0.0.1:8080  proto.Greeter.SayHello
{
  "message": "hello"
}

使用buf curl命令

该命令可用于调用HTTP RPC 端点地址,要求服务端使用gRPC或者Connect。
buf curl 命令只有一个位置参数, 即被调用的RPC方法的URL。被调用的方法取自URL路径的最后两段。分别是服务名称(全称)和方法名称

bash 复制代码
$ buf curl --schema ./helloworld.proto --data '{"name": "abc"}' --protocol grpc --http2-prior-knowledge  http://127.0.0.1:50051/helloworld.Greeter/SayHello
{
  "message": "Hello abc"
}

grpc-gateway插件

gRPC-Gateway是protoc的一个插件,它可以从protobuf服务定义生成一个反向代理服务,将RESTful HTTP API 翻译成gRPC。这个反向代理服务根据proto服务定义中的google.api.http 注解生成。

安装

bash 复制代码
$ go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway

该命令将安装protoc-gen-grpc-gateway到 $GOBIN目录,请确保 $GOBIN 在 $PATH 环境变量中.

启用REST API反向代理

预先使用protobuf定义好gRPC服务,并代码生成gRPC stub代码,像寻常一样实现gRPC API。我们将使用protoc-gen-grpc-gateway 生成反向代理。

在proto文件中添加google.api.http注解

bash 复制代码
 syntax = "proto3";
 package your.service.v1;
 option go_package = "github.com/yourorg/yourprotos/gen/go/your/service/v1";
+
+import "google/api/annotations.proto";
+
 message StringMessage {
   string value = 1;
 }

 service YourService {
-  rpc Echo(StringMessage) returns (StringMessage) {}
+  rpc Echo(StringMessage) returns (StringMessage) {
+    option (google.api.http) = {
+      post: "/v1/example/echo"
+      body: "*"
+    };
+  }
 }

自动生成反向代理代码

  • 如果使用protoc

该插件依赖googleapis的proto文件,在protoc运行插件之前,需要把这些依赖准备好,把依赖的proto文件下载到本地目录下,proto 文件在gooleapis 的github仓库里可以找到。

bash 复制代码
google/api/annotations.proto
google/api/field_behavior.proto
google/api/http.proto
google/api/httpbody.proto

然后运行protoc命令

bash 复制代码
$ protoc -I . --grpc-gateway_out ./gen/go \
    --grpc-gateway_opt paths=source_relative \
    your/service/v1/your_service.proto
  • 如果使用buf

如果使用buf,可以在buf.yaml文件的 deps字段下添加依赖的proto模块。然后记得执行buf mod update更新依赖

yaml 复制代码
#buf.yaml
version: v2
modules:
  - path: proto
lint:
  use:
    - DEFAULT
breaking:
  use:
    - FILE
+deps:
+  - buf.build/googleapis/googleapis

在 buf.gen.yaml 文件中添加grpc-gateway插件, 示例:

bash 复制代码
#buf.gen.yaml
version: v2
managed:
  enabled: true
  override:
    - file_option: go_package_prefix
      value: enginefaas.com/bufbuild/buf-tour/gen
plugins:
  # dependencies
  - remote: buf.build/protocolbuffers/go
    out: gen/go
    opt:
      - paths=source_relative  
  - remote: buf.build/grpc/go
    out: gen/go
    opt:
      - paths=source_relative
  - remote: buf.build/grpc-ecosystem/gateway:v2.21.0
    # execute 'go get github.com/grpc-ecosystem/grpc-gateway/v2@v2.21.0' before using this plugin.
    out: gen/go
    opt:
      - paths=source_relative
inputs:
  - directory: proto

然后运行buf generate命令即可

运行restful反向代理服务

我们可以在原来gRPC服务main文件的基础上添加如下代码:

go 复制代码
// Package main implements a server for Greeter service.
package main

import (
        ...
        "github.com/grpc-ecosystem/grpc-gateway/v2/runtime"
)

var (
        port = flag.Int("port", 50051, "The server port")
)

// server is used to implement helloworld.GreeterServer.
type server struct {
        pb.UnimplementedGreeterServer
}

// SayHello implements helloworld.GreeterServer
func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
        log.Printf("Received: %v", in.GetName())
        return &pb.HelloReply{Message: "Hello " + in.GetName()}, nil
}

func main() {
        flag.Parse()
        lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
        if err != nil {
                log.Fatalf("failed to listen: %v", err)
        }
        s := grpc.NewServer()
        pb.RegisterGreeterServer(s, &server{})
        log.Printf("server listening at %v", lis.Addr())
        go func() {
                if err := s.Serve(lis); err != nil {
                        log.Fatalf("failed to serve: %v", err)
                }
        }()

        // Create a client connection to the gRPC server we just started
        // This is where the gRPC-Gateway proxies the requests
        conn, err := grpc.NewClient(fmt.Sprintf("%s:%d", "127.0.0.1", *port), grpc.WithTransportCredentials(insecure.NewCredentials()))
        if err != nil {
                log.Fatalf("did not connect: %v", err)
        }

        if err != nil {
                log.Fatalln("Failed to dial rpc server:", err)
        }

        gwmux := runtime.NewServeMux()
        // Register User Service
        err = pb.RegisterGreeterHandler(context.Background(), gwmux, conn)
        if err != nil {
                log.Fatalln("Failed to register gateway handlers:", err)
        }

        gwServer := &http.Server{
                Addr:    fmt.Sprintf(":%d", 8081),
                Handler: gwmux,
        }

        log.Printf("Serving gRPC-Gateway on port %d", 8081)
        log.Fatalln(gwServer.ListenAndServe())
}

然后执行go run main.py重新运行服务,将在8081端口暴露gRPC-Gateway代理服务。

protoc-gen-openapiv2插件

安装

bash 复制代码
$ go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-openapiv2

该命令将安装protoc-gen-openapiv2到 $GOBIN目录,请确保 $GOBIN 在 $PATH 环境变量中.

生成swagger文档

  • 如果使用protoc

protoc-gen-openapiv2插件支持一些定制的protobuf注解,我们需要依赖额外的proto文件,把依赖的proto文件下载到本地目录下。在grpc-gateway 的github仓库protoc-gen-openapiv2/options目录下,可以找到proto 文件。

然后运行protoc命令

bash 复制代码
$ protoc -I . --openapiv2_out ./gen/openapiv2 \
    your/service/v1/your_service.proto
  • 如果使用buf

如果使用buf,可以在buf.yaml文件的 deps字段下添加依赖的proto模块。记得执行buf mod update更新依赖

yaml 复制代码
#buf.yaml
version: v2
modules:
  - path: proto
lint:
  use:
    - DEFAULT
breaking:
  use:
    - FILE
+deps:
+  - buf.build/grpc-ecosystem/grpc-gateway

在 buf.gen.yaml 文件中添加openapiv2插件, 示例:

yaml 复制代码
#buf.gen.yaml
version: v2
managed:
  enabled: true
  override:
    - file_option: go_package_prefix
      value: enginefaas.com/bufbuild/buf-tour/gen
plugins:
  # dependencies
  - remote: buf.build/protocolbuffers/go
    out: gen/go
    opt:
      - paths=source_relative  
  - remote: buf.build/grpc/go
    out: gen/go
    opt:
      - paths=source_relative
  - remote: buf.build/grpc-ecosystem/gateway:v2.21.0
    # execute 'go get github.com/grpc-ecosystem/grpc-gateway/v2@v2.21.0' before using this plugin.
    out: gen/go
    opt:
      - paths=source_relative
  - remote: buf.build/grpc-ecosystem/openapiv2:v2.21.0
    out: gen/openapiv2      
 inputs:
  - directory: proto

然后运行buf generate命令即可生成swagger.json文件

参考

相关推荐
hlsd#37 分钟前
go 集成go-redis 缓存操作
redis·缓存·golang
qq_1728055914 小时前
GIN 反向代理功能
后端·golang·go
__AtYou__15 小时前
Golang | Leetcode Golang题解之第535题TinyURL的加密与解密
leetcode·golang·题解
kevin_tech19 小时前
Go API 多种响应的规范化处理和简化策略
开发语言·后端·golang·状态模式
幺零九零零1 天前
【Golang】sql.Null* 类型使用(处理空值和零值)
数据库·sql·golang
cookies_s_s1 天前
Golang--DOS命令、变量、基本数据类型、标识符
golang
__AtYou__1 天前
Golang | Leetcode Golang题解之第541题反转字符串II
leetcode·golang·题解
flying robot1 天前
Go的JSON转化
golang
幺零九零零1 天前
【Golang】validator库的使用
开发语言·后端·golang
海绵宝宝de派小星1 天前
Go:接口和反射
开发语言·后端·golang