gRPC golang开发实践
- Protobuf
- Protoc使用
- Buf使用
- [实现gRPC API](#实现gRPC API)
- [测试gRPC API](#测试gRPC API)
-
- 使用bloomRPC客户端工具
- 使用grpcurl命令行工具
- [使用buf curl命令](#使用buf curl命令)
- grpc-gateway插件
-
- 安装
- [启用REST API反向代理](#启用REST API反向代理)
- protoc-gen-openapiv2插件
- 参考
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
- grpc go插件
在google.golang.org/protobuf中,protoc-gen-go纯粹用来生成pb序列化相关的文件,不再承载gRPC代码生成功能。
生成gRPC相关代码需要安装grpc-go相关的插件protoc-gen-go-grpc
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的时候,将遇到两个问题:
- 随着使用的插件逐渐增多,插件参数变多,命令行执行并不是很方便和直观。例如后面还将使用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
- 依赖某些外部的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文件