protobuf
什么是 Protocol Buffers
Protocol Buffers(简称 Protobuf)是由 Google 开发的一种语言中立、平台中立、可扩展的序列化结构数据的方法。Protobuf 在数据存储和通信协议中非常高效,广泛应用于分布式系统、网络通信、配置文件等场景。
Protobuf 的核心理念是定义数据结构,然后使用编译器生成代码,这些代码可以在不同的编程语言中使用,从而使得数据的序列化和反序列化变得非常高效。
其优势主要体现在:
- 高效:Protobuf 使用紧凑的二进制格式,序列化和反序列化速度非常快,数据传输的体积也较小。
- 跨平台:支持多种编程语言,能够在不同的平台之间无缝传输数据。
- 可扩展:允许在不破坏现有数据结构的情况下添加新的字段,这使得它非常适合长期维护和迭代开发。
- 类型安全:定义了明确的数据模型,减少了数据传输中的歧义。
protobuf 基本概念
Protobuf 使用 .proto
文件来定义数据的结构。在这个文件中,你可以定义消息(message),每个消息包含多个字段,每个字段都有一个唯一的编号。这些编号用于在序列化和反序列化过程中识别字段。
Protobuf 有两种版本的语法:proto2 和 proto3。目前,proto3 更常用。
protobuf 命令
protoc
:protobuf 编译器命令,用于将.proto
文件编译并生1成目标语言的代码;-I directory/to/proto/path
:指定.proto
文件的搜索路径。-I
选项告诉protoc
在directory/to/proto/file
目录中查找.proto
文件。
普通 pb 文件:
--go_out directory/to/pb/path
:指定生成文件的输出路径。生成 Go 语言的 Protobuf 文件,并将生成的文件放在directory/to/pb/path
目录中。--go_opt paths=source_relative
:指定生成的文件路径为相对路径,这样生成的文件路径会保留原始的目录结构。
如果要生成别的语言文件,将--go_out
改为--java_out
或--cpp_out
/--python_out
/--js_out
gRPC 的 pb 文件:
--go-grpc_out directory/to/pb/grpc/path
:指定生成 Go 语言的 gRPC 服务路径。生成 Go 语言的 gRPC 服务代码,并将生成的文件放在directory/to/pb/grpc/path
目录中。--go-grpc_opt paths=source_relative
:指定生成的文件路径为相对路径。
gRPC gateway 的 pb 文件:
--grpc-gateway_out directory/to/pb/grpc/gateway/path
:指定生成 gRPC-Gateway 代理代码路径。生成 gRPC-Gateway 代理代码,并将生成的文件放在directory/to/pb/grpc/gateway/path
目录中。--grpc-gateway_opt paths=source_relative
:指定生成的文件路径为相对路径。
指定插件的选项:
--plugin
Protobuf 语法
声明版本
syntax = "proto3"; // 告诉编译器使用 proto3 版本
声明包
package person;
声明选项(golang的包路径、包别名)
option go_package="github/grpcStudy/pb/person;person";
声明消息体
使用 message
定义消息体,消息体命名大写,每一个属性后面的数字表示其唯一标识符。
message Person{
string name = 1;
int32 age = 2;
bool sex = 3;
}
- 对于单个类型,protobuf 官网给出了 protobuf 类型与各语言的类型对照表。
protobuf 为每个字段提供的默认值:类型 默认值 string ""
byte ''
bool false
numeric(数字) 0
enum 第一个枚举值( 0
)对象类型 随语言而定 - 对于切片类型,通过前面添加关键字
repeated
示意这是一个重复字段,表示切片。 - 对于 map 类型,定义
map<TYPE, TYPE>
即可。 - 对于嵌套类型,直接使用即可。也支持嵌套定义
message TYPE {...}
一个新类型,并直接在此message
内部使用。
完整示例:
syntax = "proto3"; // 告诉编译器使用 proto3 版本
package person;
option go_package="github/grpcStudy/pb/person;person";
message Class {
repeated Person persons = 1;
message Teacher {
string name = 1;
}
Teacher teacher = 2;
}
message Person{
string name = 1;
int32 age = 2;
bool sex = 3;
repeated string hobbies = 4;
map<string, string> scores = 5;
}
预留字段名、唯一标识值
可以通过关键字 reserved
来提前声明保留一个字段名或唯一标识值,在使用 reserved
提前声明的情况下,这个字段名/唯一标识值会被不允许使用。
message Person{
string name = 1;
int32 age = 2;
bool sex = 3;
repeated string hobbies = 4;
map<string, string> scores = 5;
reserved "futureField";
reserved 10 to 19;
}
在这种情况下,Person
消息体不允许出现名称为 futureField
的属性名和 10-19
的唯一标识。
声明枚举
message Person{
string name = 1;
int32 age = 2;
enum SEX {
MALE = 0;
FEMALE = 1;
}
SEX sex = 3;
repeated string hobbies = 4;
map<string, string> scores = 5;
reserved "futureField";
reserved 10 to 19;
}
❗注:枚举定义中必须有
0
值。
如果需要在枚举中设置别名,可以通过 option allow_alias = true
来设置,例如:
message Person{
string name = 1;
int32 age = 2;
enum SEX {
option allow_alias = true;
MALE = 0;
FEMALE = 1;
BOY = 0;
GIRL = 1;
}
SEX sex = 3;
repeated string hobbies = 4;
map<string, string> scores = 5;
reserved "futureField";
reserved 10 to 19;
}
oneof
属性
oneof
是一种特殊的字段集合,表示同一时间只能设置其中的一个字段。它类似于 C 语言中的 union
,用于节省内存并防止同一时间设置多个互斥字段。
其作用有:
- 节省内存:因为
oneof
中同时只能有一个字段被设置,所以在内存中只需要分配一个字段的空间。 - 数据完整性:防止同时设置多个互斥字段,确保数据的一致性和完整性。
- 简化逻辑:在处理互斥字段时,简化了代码逻辑,不需要额外的检查和处理。
- 示例
为上面的Person
声明一个属性Married
:
想要表达的意思是:这个人要么message Person{ string name = 1; int32 age = 2; enum SEX { option allow_alias = true; MALE = 0; FEMALE = 1; BOY = 0; GIRL = 1; } SEX sex = 3; repeated string hobbies = 4; map<string, string> scores = 5; oneof Married { bool isMarried = 6; bool notMarried = 7; } reserved "futureField"; reserved 10 to 19; }
isMarried
要么notMarried
。
在 Go 项目中使用它:func main() { person := &personpb.Person{ Name: "Alice", Age: 20, Sex: personpb.Person_MALE, Hobbies: []string{"reading", "painting"}, Scores: map[string]string{"math": "A", "english": "B"}, Married: &personpb.Person_IsMarried{ IsMarried: true, }, } fmt.Println(person) }
导入其它 .proto
中的消息类型
通过 import ""
语法进行导入。
需要注意的是,这里写的相对路径并不是以当前 .proto
文件为基准,是以编译时命令的选项 --proto_path
的值作为基准目录,如果没有设置该选项,会将运行命令的目录作为基准目录。
声明服务
- 服务声明的语法:
service SERVICE_NAME { rpc FUNCTINON_NAME(Person) returns (Person); // 传统的 即刻相应的 rpc FUNCTINON_NAME_IN(stream Person) returns (Person); // 入参为流 rpc FUNCTINON_NAME_OUT(Person) returns (stream Person); // 出参为流 rpc FUNCTINON_NAME_IO(stream Person) returns (stream Person); // 出入参都为流 }
以上代码使用了声明 rpc 方法的四种方式。
声明服务后,在 protobuf 编译后会新生成 _grpc.pb.go
的文件,作为使用 Server
和 Client
的源代码。
grpc Server 的使用
普通服务
没什么特别的,要熟练掌握编写客户端和服务端的初始化代码。
- 示例
person.proto
:
syntax = "proto3"; // 告诉编译器使用 proto3 版本
package person;
option go_package="github/grpcStudy/pb/personpb;personpb";
message PersonReq{
string name = 1;
int32 age = 2;
}
message PersonRes {
string name = 1;
int32 age = 2;
}
service SearchService {
rpc Search(PersonReq) returns (PersonRes); // 传统的 即刻相应的
rpc SearchIn(stream PersonReq) returns (PersonRes); // 入参为流
rpc SearchOut(PersonReq) returns (stream PersonRes); // 出参为流
rpc SearchIO(stream PersonReq) returns (stream PersonRes); // 入参、出参均为流
}
server/main.go
(服务端):
package main
import (
"context"
"google.golang.org/grpc"
personpb "grpcStudy2/pb/person"
"log"
"net"
)
type personServer struct {
personpb.UnimplementedSearchServiceServer
}
func (*personServer) Search(ctx context.Context, req *personpb.PersonReq) (*personpb.PersonRes, error) {
name := req.GetName()
age := req.GetAge()
res := &personpb.PersonRes{
Name: "Got" + name,
Age: age,
}
return res, nil
}
func (*personServer) SearchIn(grpc.ClientStreamingServer[personpb.PersonReq, personpb.PersonRes]) error {
return nil
}
func (*personServer) SearchOut(*personpb.PersonReq, grpc.ServerStreamingServer[personpb.PersonRes]) error {
return nil
}
func (*personServer) SearchIO(grpc.BidiStreamingServer[personpb.PersonReq, personpb.PersonRes]) error {
return nil
}
func main() {
l, err := net.Listen("tcp", ":8888")
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer()
personpb.RegisterSearchServiceServer(s, &personServer{})
err = s.Serve(l)
if err != nil {
log.Fatal(err)
}
}
client/main.go
(客户端):
package main
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
personpb "grpcStudy2/pb/person"
"log"
)
func main() {
conn, err := grpc.NewClient("localhost:8888", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
log.Fatal(err)
}
defer conn.Close()
client := personpb.NewSearchServiceClient(conn)
// 普通 gRPC 服务
req, err := client.Search(context.Background(), &personpb.PersonReq{Name: "Alice", Age: 24})
if err != nil {
log.Fatal(err)
}
fmt.Println(req)
}
流式传入
流式传入,是针对服务端来说的,客户端发送流,服务端接收流,并且在停止接收流的时候服务端会给客户端一个流的反馈。
服务端
对于服务端来说,实现一个流失传入的方法时,会接收到一个 grpc.ClientStreamingServer[Req any, Res any]
的参数(Req 和 Res 是在 proto 中定义的),这个参数有以下方法:
Recv
表示接收(Receive)。
读Recv
需要在无限循环的for
中读取。for { req, err := server.Recv() fmt.Println(req) if err != nil { server.SendAndClose(&personpb.PersonRes{Name: "Error receiving request", Age: -1}) break } }
SendAndClose
表示发送一条信息表示流结束。
完整服务端方法示例:func (*personServer) SearchIn(server grpc.ClientStreamingServer[personpb.PersonReq, personpb.PersonRes]) error { for { req, err := server.Recv() fmt.Println(req) if err != nil { server.SendAndClose(&personpb.PersonRes{Name: "Error receiving request", Age: -1}) break } } return nil }
客户端
在客户端调用流式传入的方法时,会得到一个 grpc.ClientStreamingClient[Req any, Res any]
类型的对象,该对象有以下方法:
Send
表示发送信息CloseAndRecv
表示关闭请求流并等待服务端的返回。
完整客户端请求代码:// 流式传入 c, err := client.SearchIn(context.Background()) if err != nil { log.Fatal(err) } for i := 0; i < 10; i++ { time.Sleep(1 * time.Second) err = c.Send(&personpb.PersonReq{Name: fmt.Sprintf("Person%d", i), Age: 25}) if err != nil { log.Fatal(err) } } res, err := c.CloseAndRecv() fmt.Println("CloseAndRecv:", res)
流式返回
做法跟上面基本相同,具体可以通过源码看到,直接写代码:
- 服务端:
func (*personServer) SearchOut(req *personpb.PersonReq, server grpc.ServerStreamingServer[personpb.PersonRes]) error { name := req.GetName() age := req.GetAge() for i := 0; i < 10; i++ { time.Sleep(1 * time.Second) server.Send(&personpb.PersonRes{Name: fmt.Sprintf("Person%d%s", i, name), Age: age}) } return nil }
- 客户端:
// 流式返回 c, err := client.SearchOut(context.Background(), &personpb.PersonReq{Name: "Hreate", Age: 26}) if err != nil { log.Fatal(err) } for { res, err := c.Recv() if err != nil { if err == io.EOF { fmt.Println("Got All Information!!!") } else { log.Fatal(err) } break } fmt.Println("Got Res:", res) }
流式出入(双向流)
服务端:
- **第一版本:**Go 语言新手写的服务端
从该代码片段可以看出思路:func (*personServer) SearchIO(server grpc.BidiStreamingServer[personpb.PersonReq, personpb.PersonRes]) error { info := make(chan string) go func() { for { req, err := server.Recv() if err != nil { info <- "Stream End" if err == io.EOF { fmt.Println("Got All Information!!!") break } else { log.Fatal(err) } } info <- req.Name fmt.Println("Server Got Info:", req) } }() for { s := <-info if s == "Stream End" { break } err := server.Send(&personpb.PersonRes{Name: s, Age: 23}) if err != nil { if err == io.EOF { fmt.Println("Server Send Ending...") break } else { log.Fatal(err) } } } return nil }
- 服务端通过启动一个 goroutine 来接收客户端数据流,并通过一个管道 info 来将数据传出;
- 下面再通过一个循环来将模拟,从管道中获取数据,并进行相应处理,然后返回给客户端;
但是,这段代码的写法并不具有 Go 语言的标准风格,改良后:
- 第二版本:具有 Go 标准风格的服务端
func (*personServer) SearchIO(server grpc.BidiStreamingServer[personpb.PersonReq, personpb.PersonRes]) error {
reqChan := make(chan *personpb.PersonReq)
errChan := make(chan error)
go func() {
for {
req, err := server.Recv()
if err != nil {
if err == io.EOF {
// 如果请求流结束, 关闭 reqChan 管道并结束本 goroutine
close(reqChan)
return
}
errChan <- err
}
reqChan <- req
}
}()
for {
select {
case req, ok := <-reqChan:
if !ok {
// 如果 reqChan 管道关闭,表示请求流结束
fmt.Println("Got All Information!!!")
}
fmt.Println("Server Got Info:", req)
err := server.Send(&personpb.PersonRes{Name: req.Name, Age: req.Age})
if err != nil {
return err
}
case err := <-errChan:
return err
}
}
}
可以看到,比上面的版本好的不止一星半点,体现在如下几个方面:
- 第一个 goroutine 不做任何异常处理,而是通过开启一个 errChan 管道来传递异常,由主 goroutine 来统一处理异常;
- 请求流结束时,直接通过
close(reqChan)
关闭管道,而不是传一个字符串(不够具有可扩展性); - 主 goroutine 通过使用
select
针对两个管道可以非常方便的处理,即: - 如果第一个 goroutine 正常获取到了 req,会进入第一个
case
,进行相应的处理即可返回; - 如果第一个 goroutine 出现了异常,只有可能是未知异常,因为如果是请求流结束,
reqChan
会直接关闭掉,而不会发送错误给errChan
,所以主 goroutine 就可以直接将异常返回。
客户端:
// 流式出入
c, err := client.SearchIO(context.Background())
if err != nil {
log.Fatal(err)
}
wg := sync.WaitGroup{}
wg.Add(2)
go func() {
defer wg.Done()
for i := 0; i < 10; i++ {
time.Sleep(1 * time.Second)
err = c.Send(&personpb.PersonReq{Name: "Hreate", Age: 28})
if err != nil {
log.Fatal(err)
}
}
err = c.CloseSend()
if err != nil {
log.Fatal(err)
}
}()
go func() {
defer wg.Done()
for {
res, err := c.Recv()
if err != nil {
if err == io.EOF {
fmt.Println("Got All Information!!!")
break
} else {
log.Fatal(err)
}
}
fmt.Println("Client Got Res:", res)
}
}()
wg.Wait()
- 客户端通过启动两个 goroutine 来控制请求流和返回流,并通过 WaitGroup 来控制等待两个 goroutine 完成再结束此方法。
- 请求流限制发送 10 次。
- 返回流的处理对
err
进行判断,如果是io.EOF
则表示返回流结束,退出该goroutine
❗记得使用
WaitGroup
时,最好的方式是在启动每一个goroutine
时,使用defer wg.Done()
。
grpc gateway
gRPC-Gateway 是一个用于将 gRPC 服务转化为 JSON/HTTP API 的开源项目。它使得你可以在同一个服务中同时提供 gRPC 和 RESTful 接口,从而方便地集成到使用 HTTP/JSON 的现有系统中。
grpc gateway 的使用
安装 grpc gateway 的 Go 依赖
go install github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
然后添加包依赖
go get github.com/grpc-ecosystem/grpc-gateway/v2/protoc-gen-grpc-gateway@latest
添加 gRPC-Gateway 的扩展选项
在 .proto 文件中引入 gRPC-Gateway annotations,并添加 gRPC-Gateway 的扩展选项。
参考官网:添加 grpc gatewat 扩展选项
官网中有写到,这个 proto 文件定义了如何将 gRPC 服务映射到 JSON 请求和返回值,所以需要将这两个 .proto
文件下载并放到目录下。
最终的目录结构:
service SearchService {
rpc Search(PersonReq) returns (PersonRes){
option (google.api.http) = {
post: "/api/person",
body: "*"
};
}; // 传统的 即刻响应的
}
这里的 option (google.api.http)
是一个扩展选项,表示这个 gRPC 方法可以通过 HTTP 进行访问。
运行 protoc 命令,生成 gRPC 代码
在 pb 目录下运行:
protoc --go_out . --go_opt paths=source_relative --go-grpc_out . --go-grpc_opt paths=source_relative --grpc-gateway_out . --grpc-gateway_opt paths=source_relative ./person/person.proto
生成代码:
其中的 .gw.go 文件就是 gateway 文件。
在服务端创建一个 http 服务
创建一个 gRPC 连接
既然要把 grpc 映射到 HTTP 供外部访问,那当然要先连接到这个 grpc 服务了。
conn, err := grpc.NewClient("localhost:8888", grpc.WithTransportCredentials(insecure.NewCredentials()))
创建一个 ServeMux 实例,用于处理 HTTP 请求
gRPC-Gateway 的 mux
是指 ServeMux
,它是 gRPC-Gateway 框架中的一个核心组件,用于处理 HTTP 请求并将其转发到相应的 gRPC 服务。ServeMux
实现了一个反向代理功能,将 HTTP 请求映射到对应的 gRPC 方法,并将 gRPC 方法的响应转换为 HTTP 响应返回给客户端。
ServeMux 的作用:
- 请求路由:
ServeMux
负责将不同的 HTTP 请求路由到相应的 gRPC 方法。它根据 HTTP 方法(GET、POST 等)、请求路径和其他 HTTP 请求属性来决定将请求转发到哪个 gRPC 方法。 - 协议转换:
ServeMux
将 HTTP 请求转换为 gRPC 请求,并将 gRPC 响应转换为 HTTP 响应。这包括将 JSON 请求体转换为 Protobuf 格式,以及将 Protobuf 格式的响应转换为 JSON 格式。 - 中间件支持:
你可以在ServeMux
上添加中间件,如日志记录、认证、限流等。
创建一个ServeMux
实例:
mux := runtime.NewServeMux()
创建一个 HTTP 服务
创建一个 HTTP 服务,并注册 gRPC 服务的 HTTP 处理程序,将 HTTP 请求转发到 gRPC 服务器。
gwServer := &http.Server{
Handler: mux,
Addr: ":8090",
}
err = personpb.RegisterSearchServiceHandler(context.Background(), mux, conn)
if err != nil {
log.Fatal(err)
}
监听网关服务
最后,监听网关服务
gwServer.ListenAndServe()
服务端整体示例代码
func main() {
wg := sync.WaitGroup{}
wg.Add(2)
go registerGRPCGateway(&wg)
go registerGRPCServer(&wg)
wg.Wait()
}
func registerGRPCGateway(wg *sync.WaitGroup) {
defer wg.Done()
conn, err := grpc.NewClient("localhost:8888", grpc.WithTransportCredentials(insecure.NewCredentials()))
mux := runtime.NewServeMux()
gwServer := &http.Server{
Handler: mux,
Addr: ":8090",
}
err = personpb.RegisterSearchServiceHandler(context.Background(), mux, conn)
if err != nil {
log.Fatal(err)
}
err = gwServer.ListenAndServe()
if err != nil {
log.Fatal(err)
}
}
func registerGRPCServer(wg *sync.WaitGroup) {
defer wg.Done()
l, err := net.Listen("tcp", ":8888")
if err != nil {
log.Fatal(err)
}
s := grpc.NewServer()
personpb.RegisterSearchServiceServer(s, &personServer{})
err = s.Serve(l)
if err != nil {
log.Fatal(err)
}
}
详解 grpc gateway 的 options
.proto
文件中 option (google.api.http)
是一个扩展选项,用于在 Protobuf 文件中定义 gRPC 方法的 HTTP 映射规则。这些规则包括 HTTP 方法、请求路径、请求体等。
HTTP 方法选项
gRPC-Gateway 支持多种 HTTP 方法和丰富的配置选项。以下是 gRPC-Gateway 中常用的 HTTP 映射选项:
get
: 用于映射 HTTP GET 请求。post
: 用于映射 HTTP POST 请求。put
: 用于映射 HTTP PUT 请求。patch
: 用于映射 HTTP PATCH 请求。delete
: 用于映射 HTTP DELETE 请求。
默认的 query 入参
如果定义的 option
中既没有以 path
也没有以 body
的形式定义入参,默认会支持以 query
的形式入参。
比如以下 rpc :
rpc GetPerson2(PersonReq) returns (PersonRes) {
option (google.api.http) = {
get: "/api/person"
};
};
该 rpc 生成 HTTP 接口后,支持以 query 的形式入参:
并且还支持传入嵌套属性:
rpc GetPerson3(PersonReq2) returns (PersonRes2) {
option (google.api.http) = {
get: "/api/person2"
};
};
调用:
path 入参
path
入参可以通过在 uri 中定义,并且:
path 入参支持嵌套属性
示例:
rpc GetPerson4(PersonReq2) returns (PersonRes2) {
option (google.api.http) = {
get: "/api/person2/{name}/{age}/{dog.name}"
};
};
调用:
path 入参设置为固定值
path 入参支持可以设置为固定值(如果传入的不为固定值会 NOT FOUND)
示例:
rpc GetPerson4(PersonReq2) returns (PersonRes2) {
option (google.api.http) = {
get: "/api/person2/{name=hreate}/{age}/{dog.name}"
};
};
故意传入别的 name :
结果:404 NOT FOUND
传入预先设置的 hreate
:
成功。
body 选项
body
选项用于指定 HTTP 请求体如何映射到 gRPC 请求消息。它有以下几种常见的设置方式:
body: "*"
: 表示整个 HTTP 请求体都映射到 gRPC 请求消息。body: "field_name"
: 表示 HTTP 请求体中的某个字段映射到 gRPC 请求消息的指定字段。❗注:
如果在body
中指定了字段,这个接口就不支持传递整个 JSON 对象,只能传递这个字段对应的值。
比如:
有一PersonReq
结构体有属性name
和age
,在某一POST
接口中定义了body: "name"
,那在传递时只能传递:"NAME_CONTENT"
,而不支持传递:{ "name": "NAME_CONTENT" }
- 省略
body
选项: 表示不使用请求体,所有参数都通过 URL 查询参数传递。
additional_bindings 选项
additional_bindings
允许你为同一个 gRPC 方法定义多个 HTTP 映射。这在需要支持多个 URL 路径或 HTTP 方法访问同一个 gRPC 方法时非常有用。
-
示例
假设有一个GetPerson
方法,既希望通过GET
请求获取用户信息,也希望通过POST
请求获取用户信息:syntax = "proto3"; package example; import "google/api/annotations.proto"; message GetPersonReq { string id = 1; } message PersonRes { string name = 1; int32 age = 2; } service PersonService { rpc GetPerson(GetPersonReq) returns (PersonRes) { option (google.api.http) = { get: "/api/person/{id}" additional_bindings { post: "/api/person" body: "*" } }; } }
以上代码中,通过使用
additional_bindings
定义了一个额外的 HTTP 映射。这样,
GetPerson
方法既可以通过GET /api/person/{id}
访问,也可以通过POST /api/person
并在请求体中传递GetPersonReq
对象访问。
response_body 选项
response_body
允许你指定 gRPC 响应消息的哪个部分映射到 HTTP 响应体,在只希望返回响应消息的一部分时这个选项非常有用。
- 示例
假设你有一个CreatePerson
方法,创建用户后希望只返回创建的用户的id
而不是整个用户对象:
这样,当调用syntax = "proto3"; package example; import "google/api/annotations.proto"; message CreatePersonReq { string name = 1; int32 age = 2; } message CreatePersonRes { string id = 1; string name = 2; int32 age = 3; } service PersonService { rpc CreatePerson(CreatePersonReq) returns (CreatePersonRes) { option (google.api.http) = { post: "/api/person" body: "*" response_body: "id" }; } }
CreatePerson
方法时,HTTP 响应体将只包含创建的用户的id
,而不是整个CreatePersonRes
对象。