9-protobuf进阶
官方地址: https://developers.google.com/protocol-buffers/docs/proto3
一 protobuf 基本类型和默认值
1.1 protobuf类型和语言对应关系
该表格展示了定义于.proto文件中的类型,与go和python对应的类型:
.proto Type | Notes | Python Type | Go Type |
---|---|---|---|
double | float | float64 | |
float | float | float32 | |
int32 | 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 | int | int32 |
uint32 | 使用变长编码 | int | uint32 |
uint64 | 使用变长编码 | int | uint64 |
sint32 | 使用变长编码,这些编码在负值时比int32高效的多 | int | int32 |
sint64 | 使用变长编码,有符号的整型值。编码时比通常的int64高效。 | int | int64 |
fixed32 | 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效。 | int | uint32 |
fixed64 | 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效。 | int | uint64 |
sfixed32 | 总是4个字节 | int | int32 |
sfixed64 | 总是8个字节 | int | int64 |
bool | bool | bool | |
string | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 | str | string |
bytes | 可能包含任意顺序的字节数据。 | str | []byte |
可以在文章Protocol Buffer 编码中,找到更多"序列化消息时各种类型如何编码"的信息。
- 在java中,无符号32位和64位整型被表示成他们的整型对应形似,最高位被储存在标志位中。
- 对于所有的情况,设定值会执行类型检查以确保此值是有效。
- 64位或者无符号32位整型在解码时被表示成为ilong,但是在设置时可以使用int型值设定,在所有的情况下,值必须符合其设置其类型的要求。
- python中string被表示成在解码时表示成unicode。但是一个ASCIIstring可以被表示成str类型。
- Integer在64位的机器上使用,string在32位机器上使用
1.2 protobuf默认值
如果protobuf定义了类型,在gRPC使用过程中没有传值,会使用默认值
当一个消息被解析的时候,如果被编码的信息不包含一个特定的元素,被解析的对象锁对应的域被设置位一个默认值,对于不同类型指定如下:
- 对于strings,默认是一个空string
- 对于bytes,默认是一个空的bytes
- 对于bools,默认是false
- 对于数值类型,默认是0
- 对于枚举,默认是第一个定义的枚举值,必须为0;
- 对于消息类型(message),域没有被设置,确切的消息是根据语言确定的,详见generated code guide
对于可重复域的默认值是空(通常情况下是对应语言中空列表)。
注:对于标量消息域,一旦消息被解析,就无法判断域释放被设置为默认值(例如,例如boolean值是否被设置为false)还是根本没有被设置。你应该在定义你的消息类型时非常注意。例如,比如你不应该定义boolean的默认值false作为任何行为的触发方式。也应该注意如果一个标量消息域被设置为标志位,这个值不应该被序列化传输。
查看generated code guide选择你的语言的默认值的工作细节
演示案例
目录结构
proto_default_demo
-client
main.go
-proto
hello.proto
-server
main.go
hello.proto
syntax = "proto3";
option go_package = ".;proto";
// 定义一个服务,gRPC自有的,它需要用grpc插件生成,也就是咱们安装的那个插件
service Hello{
// 服务内有一个函数叫Hello,接收HelloRequest类型参数,返回HelloResponse类型参数
rpc Hello(HelloRequest) returns(HelloResponse);
}
// 类似于go的结构体,可以定义属性
message HelloRequest {
string name = 1; // 1 是编号,不是值
int32 age = 2;
repeated string girls = 3;
}
// 定义一个响应的类型
message HelloResponse {
string reply =1;
}
生成go文件
//protoc --go_out=. --go_opt=paths=source_relative ./hello.proto
//protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false --go-grpc_opt=paths=source_relative ./hello.proto
client/main.go
package main
import (
"context"
"fmt"
"go_test_learn/proto_default_demo/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("连接服务异常")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
request := proto.HelloRequest{} // 不传值,看服务端打印
res, err := client.Hello(context.Background(), &request)
if err != nil {
panic("调用方法异常")
}
//打印出返回的数据
fmt.Println(res.Reply)
}
server/main.go
package main
import (
"context"
"fmt"
"go_test_learn/proto"
"google.golang.org/grpc"
"net"
)
type HelloServer struct {
}
func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println(request.Name) //此处打印出默认值
return &proto.HelloResponse{Reply:"收到客户端的消息为:"+request.Name}, nil
}
func main() {
g := grpc.NewServer()
s := HelloServer{}
proto.RegisterHelloServer(g,&s)
lis,error:=net.Listen("tcp","0.0.0.0:50052")
if error!=nil{
panic("启动服务异常")
}
g.Serve(lis)
}
二 option go_package的作用
可以为.proto文件新增一个可选的package声明符,用来防止不同的消息类型有命名冲突,区分语言,go_package为go语言的定义
// 基本使用
option go_package = ".;proto";
// 这样会把go文件生成到当前路径,并且go文件的包名为proto
// 指定生成的go文件放到某个路径下
option go_package = "common/hello/proto/v1";
// 这样会把go文件生成到当前路径下的common/hello/proto/文件夹中,包名为v1
// 使用命令生成
protoc --go_out=. ./hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./hello.proto
// 指定生成的go文件放到某个路径下
option go_package = "../../common/hello/proto/v1";
// 这样会把go文件生成到当前路径下的上两级目录的common/hello/proto/文件夹中,包名为v1
// 这样便于以后公共的生成到一起,多个微服务共用同样的文件
// 使用命令生成
protoc --go_out=. ./hello.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./hello.proto
三 服务端客户端同步问题
因为客户端和服务端要使用同一个proto文件,可能是两个人写的,如果两个proto文件内容不一致,会导致错误
3.1 顺序导致的错误
dif_proto
-server
-main.go
-proto
-hello.proto // 该文件应该和client下的文件完全一致
-client
-main.go
-proto
-hello.proto // 该文件应该和server下的文件完全一致
server/proto/hello.proto
syntax = "proto3";
option go_package = ".;proto";
service Hello{
rpc Hello(HelloRequest) returns(HelloResponse);
}
message HelloRequest {
string name = 1; // 服务端编号为 name是1,gender 是2
string gender = 2;
}
message HelloResponse {
string reply =1;
}
client/proto/hello.proto
syntax = "proto3";
option go_package = ".;proto";
service Hello{
rpc Hello(HelloRequest) returns(HelloResponse);
}
message HelloRequest {
// 客户端编号与服务端编号顺序不一致 name是2,gender 是1
string gender = 1;
string name = 2;
}
// 定义一个响应的类型
message HelloResponse {
string reply =1;
}
生成go文件
cd dif_proto/client/proto
//protoc --go_out=. ./hello.proto
//protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./hello.proto
cd dif_proto/server/proto
//protoc --go_out=. ./hello.proto
//protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./hello.proto
client/main.go
package main
import (
"context"
"fmt"
"go_test_learn/dif_proto/client/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("连接服务异常")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
request := proto.HelloRequest{
Name: "lqz",
Gender: "男",
}
res, err := client.Hello(context.Background(), &request)
if err != nil {
panic("调用方法异常")
}
//打印出返回的数据
fmt.Println(res.Reply)
}
server/main.go
package main
import (
"context"
"fmt"
"go_test_learn/dif_proto/server/proto"
"google.golang.org/grpc"
"net"
)
type HelloServer struct {
}
func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println("name:",request.Name)
fmt.Println("gender:",request.Gender)
return &proto.HelloResponse{Reply:"ok"}, nil
}
func main() {
g := grpc.NewServer()
s := HelloServer{}
proto.RegisterHelloServer(g,&s)
lis,error:=net.Listen("tcp","0.0.0.0:50052")
if error!=nil{
panic("启动服务异常")
}
g.Serve(lis)
}
结果
3.2 服务端数据多,客户端数据少
这样不客户端和服务端都能运行,只是数据会少
//client/proto/hello.proto
message HelloRequest {
string name = 1;
string gender = 2;
}
//server/proto/hello.proto
message HelloRequest {
string name = 1;
}
// 重新命令生成go文件,重新运行客户端和服务端
// 程序正常运行
四 import另一个proto
4.1 引入自定义的proto
// 目录结构
import_proto
-client
-main.go
-server
-main.go
-proto
-order.proto
-base.proto
proto/order.proto
syntax = "proto3";
import "base.proto"; // 导入另一个proto,就可以使用里面的Empty和Pong了
option go_package = ".;proto";
service Hello{
rpc Hello(HelloRequest) returns(HelloResponse);
// 因为 rpc的服务必须要传一个参数,但是有时候我们不需要传参数,所以定义一个Empty的message
rpc Ping(Empty) returns(Pong);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string reply =1;
}
oproto/order.proto
syntax = "proto3";
option go_package = ".;proto"; // 此处不要忘了加入包的声明
message Empty {
}
// 定义一个Pong
message Pong {
int32 code =1;
}
命令生成go文件
// 切换到相应路径下
protoc --go_out=. ./order.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./order.proto
// base.proto也要生成
protoc --go_out=. ./base.proto
client/main.go
package main
import (
"context"
"fmt"
"go_test_learn/import_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("连接服务异常")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
request := proto.Empty{}
res, err := client.Ping(context.Background(), &request)
if err != nil {
panic("调用方法异常")
}
//打印出返回的数据
fmt.Println(res.Code)
}
server/main.go
package main
import (
"context"
"fmt"
"go_test_learn/import_proto/proto"
"google.golang.org/grpc"
"net"
)
type HelloServer struct {
}
func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println(request.Name) //此处打印出默认值
return &proto.HelloResponse{Reply: "收到客户端的消息为:" + request.Name}, nil
}
func (s *HelloServer) Ping(context context.Context, request *proto.Empty) (*proto.Pong, error) {
fmt.Println(request) //request是Empty的对象,没有值
return &proto.Pong{Code: 100}, nil
}
func main() {
g := grpc.NewServer()
s := HelloServer{}
proto.RegisterHelloServer(g, &s)
lis, error := net.Listen("tcp", "0.0.0.0:50052")
if error != nil {
panic("启动服务异常")
}
g.Serve(lis)
}
4.2 引入内置的proto
proto/order.proto
syntax = "proto3";
import "base.proto"; // 导入另一个proto,就可以使用里面的Empty和Pong了
// 引入谷歌提供的,必须要使用protoc/include/google/protobuf内带empty.proto文件,否则报错
import "google/protobuf/empty.proto"; // 谷歌内置了empty给咱们用,按住control可以看源码,内有go_package是go包导入路径
option go_package = ".;proto";
service Hello{
rpc Hello(HelloRequest) returns(HelloResponse);
// 因为 rpc的服务必须要传一个参数,但是有时候我们不需要传参数,所以定义一个Empty的message
// rpc Ping(Empty) returns(Pong);
// 此处使用必须用google.protobuf.Empty
rpc Ping(google.protobuf.Empty) returns(Pong);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
string reply = 1;
}
oproto/order.proto
syntax = "proto3";
option go_package = ".;proto"; // 此处不要忘了加入包的声明
// 定义一个Pong
message Pong {
int32 code =1;
}
命令生成go文件
// 切换到相应路径下
protoc --go_out=. ./order.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./order.proto
// base.proto也要生成
protoc --go_out=. ./base.proto
// 删除已下载的模块缓存
go clean --modcache
client/main.go
package main
import (
"context"
"fmt"
"github.com/golang/protobuf/ptypes/empty" // empty使用这个包下的
"go_test_learn/import_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("连接服务异常")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
request := empty.Empty{}
res, err := client.Ping(context.Background(), &request)
if err != nil {
panic("调用方法异常")
}
//打印出返回的数据
fmt.Println(res.Code)
}
server/main.go
package main
import (
"context"
"fmt"
"github.com/golang/protobuf/ptypes/empty" // empty使用这个包下的
"go_test_learn/import_proto/proto"
"google.golang.org/grpc"
"net"
)
type HelloServer struct {
}
func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println(request.Name) //此处打印出默认值
return &proto.HelloResponse{Reply: "收到客户端的消息为:" + request.Name}, nil
}
func (s *HelloServer) Ping(context context.Context, request *empty.Empty) (*proto.Pong, error) {
fmt.Println(request) //request是Empty的对象,没有值
return &proto.Pong{Code: 100}, nil
}
func main() {
g := grpc.NewServer()
s := HelloServer{}
proto.RegisterHelloServer(g, &s)
lis, error := net.Listen("tcp", "0.0.0.0:50052")
if error != nil {
panic("启动服务异常")
}
g.Serve(lis)
}
五 嵌套message对象
proto文件的message可以嵌套其他的message,修改order.proto如下
order.proto
syntax = "proto3";
import "base.proto"; // 导入另一个proto,就可以使用里面的Empty和Pong了
import "google/protobuf/empty.proto"; // 谷歌内置了empty给咱们用,按住control可以看源码,内有go_package是go包导入路径
option go_package = ".;proto";
service Hello{
rpc Hello(HelloRequest) returns(HelloResponse);
// 因为 rpc的服务必须要传一个参数,但是有时候我们不需要传参数,所以定义一个Empty的message
// rpc Ping(Empty) returns(Pong);
// 此处使用必须用google.protobuf.Empty
rpc Ping(google.protobuf.Empty) returns(Pong);
}
message HelloRequest {
string name = 1;
}
message HelloResponse {
message Result { // 嵌套Result,也可以放在message HelloResponse外面
string code = 1;
string msg = 2;
}
string reply = 1;
Result data = 2;
}
命令生成go文件
// 切换到相应路径下
protoc --go_out=. ./order.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./order.proto
// base.proto也要生成
protoc --go_out=. ./base.proto
client/main.go
package main
import (
"context"
"fmt"
"go_test_learn/import_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("连接服务异常")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
request := proto.HelloRequest{Name: "lqz"}
res, err := client.Hello(context.Background(), &request)
if err != nil {
panic("调用方法异常")
}
//message嵌套,可以嵌套获取
fmt.Println(res.Data.Code)
fmt.Println(res.Data.Msg)
}
server/main.go
package main
import (
"context"
"fmt"
"github.com/golang/protobuf/ptypes/empty" // empty使用这个包下的
"go_test_learn/import_proto/proto"
"google.golang.org/grpc"
"net"
)
type HelloServer struct {
}
func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println(request.Name) //此处打印出默认值
return &proto.HelloResponse{
Reply: "收到客户端的消息为:" + request.Name,
Data: &proto.HelloResponse_Result{Code:"100",Msg: "成功"}, // 注意此处,嵌套的Result变成了HelloResponse_Result
}, nil
}
func (s *HelloServer) Ping(context context.Context, request *empty.Empty) (*proto.Pong, error) {
fmt.Println(request) //request是Empty的对象,没有值
return &proto.Pong{Code: 100}, nil
}
func main() {
g := grpc.NewServer()
s := HelloServer{}
proto.RegisterHelloServer(g, &s)
lis, error := net.Listen("tcp", "0.0.0.0:50052")
if error != nil {
panic("启动服务异常")
}
g.Serve(lis)
}
六 enum枚举类型
syntax = "proto3";
option go_package = ".;proto";
service Hello{
rpc Hello(HelloRequest) returns(HelloResponse);
}
message HelloRequest {
string name = 1;
Gender gen =2; // 使用枚举类型
}
message HelloResponse {
string reply = 1;
}
// 定义枚举类型
enum Gender{
Male = 0;
Female = 1;
}
命令生成go文件
// 切换到相应路径下
protoc --go_out=. ./order.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./order.proto
server/main.go
package main
import (
"context"
"fmt"
"go_test_learn/import_proto/proto"
"google.golang.org/grpc"
"net"
)
type HelloServer struct {
}
func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println(request.Name) //此处打印出默认值
return &proto.HelloResponse{
Reply: "收到客户端的性别为:" + request.Gen.String(),
}, nil
}
func main() {
g := grpc.NewServer()
s := HelloServer{}
proto.RegisterHelloServer(g, &s)
lis, error := net.Listen("tcp", "0.0.0.0:50052")
if error != nil {
panic("启动服务异常")
}
g.Serve(lis)
}
client/main.go
package main
import (
"context"
"fmt"
"go_test_learn/import_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("连接服务异常")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
// 使用枚举类型Gen: proto.Gender_Male,本质是int32
request := proto.HelloRequest{Name: "lqz",Gen: proto.Gender_Male}
res, err := client.Hello(context.Background(), &request)
if err != nil {
panic("调用方法异常")
}
fmt.Println(res.Reply)
}
七 map类型
order.proto
syntax = "proto3";
option go_package = ".;proto";
service Hello{
rpc Hello(HelloRequest) returns(HelloResponse);
}
message HelloRequest {
string name = 1;
Gender gen =2; // 使用枚举类型
map<string,string> info =3; // map类型,要指定key和value的类型
}
message HelloResponse {
string reply = 1;
}
// 定义枚举类型
enum Gender{
Male = 0;
Female = 1;
}
命令生成go文件
// 切换到相应路径下
protoc --go_out=. ./order.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./order.proto
server/main.go
package main
import (
"context"
"fmt"
"go_test_learn/import_proto/proto"
"google.golang.org/grpc"
"net"
)
type HelloServer struct {
}
func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println(request.Name) //此处打印出默认值
// 取出map类型
return &proto.HelloResponse{
Reply: "收到客户端的map:" + request.Info["name"],
}, nil
}
func main() {
g := grpc.NewServer()
s := HelloServer{}
proto.RegisterHelloServer(g, &s)
lis, error := net.Listen("tcp", "0.0.0.0:50052")
if error != nil {
panic("启动服务异常")
}
g.Serve(lis)
}
client/main.go
package main
import (
"context"
"fmt"
"go_test_learn/import_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("连接服务异常")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
// 使用map类型,实质给映射成map[string]string
request := proto.HelloRequest{Name: "lqz",
Gen: proto.Gender_Male,
Info:map[string]string{"name":"lqz","age":"19"},
}
res, err := client.Hello(context.Background(), &request)
if err != nil {
panic("调用方法异常")
}
fmt.Println(res.Reply)
}
八 内置的timestamp类型
order.proto
syntax = "proto3";
import "google/protobuf/timestamp.proto"; // 先引入
option go_package = ".;proto";
service Hello{
rpc Hello(HelloRequest) returns(HelloResponse);
}
message HelloRequest {
string name = 1;
Gender gen =2; // 使用枚举类型
map<string,string> info =3; // map类型,要指定key和value的类型
google.protobuf.Timestamp now =4; // 使用Timestamp时间类型
}
message HelloResponse {
string reply = 1;
}
// 定义枚举类型
enum Gender{
Male = 0;
Female = 1;
}
命令生成go文件
// 切换到相应路径下
protoc --go_out=. ./order.proto
protoc --go-grpc_out=. --go-grpc_opt=require_unimplemented_servers=false ./order.proto
server/main.go
package main
import (
"context"
"fmt"
"go_test_learn/import_proto/proto"
"google.golang.org/grpc"
"net"
)
type HelloServer struct {
}
func (s *HelloServer) Hello(context context.Context, request *proto.HelloRequest) (*proto.HelloResponse, error) {
fmt.Println(request.Name) //此处打印出默认值
// 取出map类型
return &proto.HelloResponse{
Reply: "收到客户端的时间:" + request.Now.AsTime().String(),
}, nil
}
func main() {
g := grpc.NewServer()
s := HelloServer{}
proto.RegisterHelloServer(g, &s)
lis, error := net.Listen("tcp", "0.0.0.0:50052")
if error != nil {
panic("启动服务异常")
}
g.Serve(lis)
}
client/main.go
package main
import (
"context"
"fmt"
"go_test_learn/import_proto/proto"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"time"
// "github.com/golang/protobuf/ptypes/timestamp" // 不导入这个路径
timestamppb "google.golang.org/protobuf/types/known/timestamppb" //上指向这个,直接导入这个路径
)
func main() {
conn, err := grpc.Dial("127.0.0.1:50052", grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
panic("连接服务异常")
}
defer conn.Close()
client := proto.NewHelloClient(conn)
request := proto.HelloRequest{Name: "lqz",
Gen: proto.Gender_Male,
Info:map[string]string{"name":"lqz","age":"19"},
Now: timestamppb.New(time.Now()), // timestamppb有New方法,传入Time对象即可
}
res, err := client.Hello(context.Background(), &request)
if err != nil {
panic("调用方法异常")
}
fmt.Println(res.Reply)
}