内容简介:Protocol Buffer是Google的语言中立的,平台中立的,可扩展机制的,用于序列化结构化数据 - 对比XML,但更小,更快,更简单。您可以定义数据的结构化,然后可以使用特殊生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据。主要有点有:我使用的proto版本是protobuf3,关于proto的学习网络上已有许多优秀的文章,在这不再赘述。 本文只介绍我在使用protobuf过程中收获的经验和遇到的坑以及如何解决的。
Protocol Buffer是Google的语言中立的,平台中立的,可扩展机制的,用于序列化结构化数据 - 对比XML,但更小,更快,更简单。您可以定义数据的结构化,然后可以使用特殊生成的源代码轻松地在各种数据流中使用各种语言编写和读取结构化数据。
主要有点有:
-
1.protoBuf在Google内部长期使用,产品稳定成熟,很多商业的项目都选择使用
-
3.protoBuf编码后消息更小、有利于存储传输
-
4.编码和解码的效率非常之高
-
5.支持不同版本的协议向前兼容
我使用的proto版本是protobuf3,关于proto的学习网络上已有许多优秀的文章,在这不再赘述。 本文只介绍我在使用protobuf过程中收获的经验和遇到的坑以及如何解决的。
单独一个项目?
如果要在多个项目中共用proto文件,最好的解决办法是单独拉出来一个git项目来管理proto文件。在笔者的项目中,有服务端、游戏客户端、web客户端共用proto项目
文件结构划分
多个项目共用proto,每个项目对proto文件的需求可能不一致,服务端可能需要全部的proto定义;游戏客户端和web客户端根据业务不同,可能只需要其中的一部分,或者对于关于 service
与 grpc
的定义,客户端一般都是不需要的(起码我们的项目中不需要)。
将proto文件进行合理的拆分,将会大大减小客户端编译后的proto文件体积 。在我们的项目中,在没有划分之前,客户端文件有1M多,划分之后只有300K左右
笔者的思路是:把一个模块里的proto划分为xx.basic.proto、xx.service.proto、xx.api.proto, 其中basic.proto 定义一些基本数据结构,service.proto 定义服务端服务,api.proto 定义http api服务, service和api都引用basic , 例如:
test.basic.proto
syntax = "proto3"; package test.basic; option go_package = "xxxxxx/.go/test"; message Message { int32 i = 1; } 复制代码
test.service.proto
syntax = "proto3"; package test.service; option go_package = "xxxxxx/.go/test"; import "test/test.basic.proto"; service Test { rpc Hello(HelloRequest) returns(HelloResponse) {} } message HelloRequest { } message HelloResponse { } service TestGrpc { rpc Stream(stream test.basic.Message) returns(stream test.basic.Message) {} } 复制代码
test.api.proto
syntax = "proto3"; package test.api; option go_package = "xxxxxx/.go/test"; service TestApi { rpc SayHello(SayHelloRequest) returns(SayHelloResponse) {} } message SayHelloRequest { } message SayHelloResponse { } 复制代码
这样的话,各个项目只需要用脚本选择自己的模块,模块中需要的proto文件,按需索取即可
编译golang
proto编译golang使用protoc插件( 项目地址 )
如果按照上文进行proto文件拆分,又需要把生成的文件导出到一个golang包里,如果单独编译是不能跑起来的,因为有文件引用的存在。所以需要一次性导入该包下所有的proto文件 *.proto
,笔者写了个入门级的python脚本辅助这一过程
build.py
import os def genProto(): print('操作系统:', os.name) fileList = os.listdir() folderList = [] # 过滤掉隐藏文件夹 例如.git .vscode for i in range(0, len(fileList)): fileName = fileList[i] dotIndex = fileName.find('.') if (dotIndex < 0): folderList.append(fileName) print("folderList:", folderList) # 每个模块逐个编译 for folderName in folderList: os.system('bash buildProto.sh ' + "../../../ " + folderName) genProto() 复制代码
buildProto.sh
echo "编译$2.proto" protoc -I . --go_out=plugins=grpc:$1 --micro_out=plugins=grpc:$1 $2/*.proto 复制代码
执行build.py,可在当前项目中把proto编译到.go文件夹里,每个模块一个golang包,达到了预期
关于 ../../../
os.system('bash buildProto.sh ' + "../../../ " + folderName)
运行buildProto.sh脚本传入了第一个参数"../../../",这个与使用时golang的导入路径和 option go_package = "xxxxxx/.go/test";
有关系。在服务端项目中使用编译后的golang文件 import "gitlab.com/xxx/xxx/.go/item"
,如果这个proto项目你是 go get
拉取下来的,文件结构会是 $GOPATH/src/xxxx/xxxx/xxxx/.go
,编译生成的文件也需要按照这个结构展开,所以需要告诉protoc --go_out=../../../
, 这一点可以根据自己情况定制
编译js/ts
npm install protobufjs
安装pbjs 项目地址
gulp脚本
var gulp = require('gulp'); var rename = require('gulp-rename'); var shell = require('gulp-shell'); var gulpSequence = require('gulp-sequence'); // 拷贝需要的proto gulp.task('copy', ['clear'], () => { return gulp .src([ `../path to your proto/*/*.basic.proto`, ]) .pipe(rename({ dirname: '' })) .pipe(gulp.dest(`protos/`)); }); gulp.task('clear', shell.task(['rm -rf protos'])); gulp.task('genProto', shell.task(['sh buildProto.sh'])); 复制代码
buildProto.sh
# 生成js 为了节省空间 去掉了许多东西 pbjs -t static-module -w commonjs -o ./buildOut/proto.js ./protos/*.proto --no-create --no-verify --no-convert --no-delimited --no-beautify --no-comments # 生成 .d.ts pbts -o ./buildOut/proto.d.ts ./buildOut/proto.js 复制代码
不友好的oneof
在定义双向流stream时 rpc Stream(stream test.basic.Message) returns(stream test.basic.Message) {}
如果Message内容比较简单就能满足需求了,但是假如像我们的游戏需要对Message的内容进行分类:
1. req: 客户端请求,要求服务端响应 2. notify: 客户端通知,不要求服务端响应 3. rsp: 服务端响应(被动) 4. event:服务端推送事件(主动) 复制代码
那么就需要一个解析Message的机制。同事提出了使用key当message 名字,写一个for循环遍历的方案,这样甚至能同事发出去多条请求、多条事件,但最终觉得这样会涉及到对key的 排序 问题最终没有采用,而是使用了proto的oneof 语法
message Message { Req req = 1; Rsp rsp = 2; Notify notify = 3; Event event = 4; } message Req { oneof req { AuthReq authReq = 1; } } message AuthReq { } message Notify { oneof notify { HiNotify hiNotify = 1; } } message HiNotify { } message Rsp { oneof rsp { AuthRsp authRsp = 1; } } message AuthRsp { } message Event { oneof Event { FooEvent fooEvent = 1; } } message FooEvent { } 复制代码
oneof字段之间是共享内存的,同一时间只能设置其中一个,其他的会被清除,因此特别节约内存。业务代码在使用起来比如key当meesage名字也更加清晰明了(添加一个字段 代码只需要在switch中添加一个case即可),只不过有两个小坑:
-
对golang不太友好: 如果要创建一个message,需要这样写
pb.Message{Req: &pb.Req{Req: &pb.Req_AuthReq{AuthReq: &pb.AuthReq{}}}}
一大长串。。。查看生成的源码可得知,之所以这样是因为golang是通过接口实现 oneof的,因此只能一层一层包下去 -
json无法解析: 上面的请求转成json为
{"req":{"authReq":{}}}
,但这个字符串无法直接转成proto,需要先把{"authReq":{}
转成authReq
,再包装成pb.Message
。如果前后端使用 arrayBuffer 则没有这个问题。
对于第一个问题,写好几个辅助函数即可弥补;对于第二个问题,在我们的项目中只有很少数的http接口使用json并且碰到了oneof,因此一直在使用中
json
默认情况下,当需要将proto转成json返回给http接口时(假如http返回的数据格式为json),那么对于字段的零值,将会被忽略。查看生成的pb源码,会发现
type Message struct { Req *Req `protobuf:"bytes,1,opt,name=req,proto3" json:"req,omitempty"` Rsp *Rsp `protobuf:"bytes,2,opt,name=rsp,proto3" json:"rsp,omitempty"` Notify *Notify `protobuf:"bytes,3,opt,name=notify,proto3" json:"notify,omitempty"` Event *Event `protobuf:"bytes,4,opt,name=event,proto3" json:"event,omitempty"` XXX_NoUnkeyedLiteral struct{} `json:"-"` XXX_unrecognized []byte `json:"-"` XXX_sizecache int32 `json:"-"` } 复制代码
这些字段被加上了 json:"omitempty"
的tag,最可气的是这个tag是protoc 写死的... 解决办法有
- 修改protoc源码自定义这个行为
- 自定义Marshaler
m := jsonpb.Marshaler{EmitDefaults: true} 复制代码
- 使用脚本移除这个标记,修改上面的build.py
import os def changeFile(fileName, old_str, new_str): file_data = "" with open(fileName, "r", encoding="utf-8") as f: for line in f: if old_str in line: line = line.replace(old_str, new_str) file_data += line with open(fileName, "w", encoding="utf-8") as f: f.write(file_data) def genProto(): print('操作系统:', os.name) fileList = os.listdir() folderList = [] # 过滤掉隐藏文件夹 例如.git .vscode for i in range(0, len(fileList)): fileName = fileList[i] dotIndex = fileName.find('.') if (dotIndex < 0): folderList.append(fileName) print("folderList:", folderList) # 每个模块逐个编译 for folderName in folderList: os.system('bash buildProto.sh ' + "../../../ " + folderName) # 换掉go里的标记 goFiles = os.listdir('.go/' + folderName) for i in range(0, len(goFiles)): fileName = goFiles[i] dotIndex = fileName.find('.pb.go') if (dotIndex >= 0): # print("替换文件:", fileName) changeFile('.go/' + folderName + '/' + fileName, ',omitempty', '') genProto() 复制代码
本人学习golang、micro、k8s、grpc、protobuf等知识的时间较短,如果有理解错误的地方,欢迎批评指正,可以加我微信一起探讨学习
以上所述就是小编给大家介绍的《牌类游戏使用微服务重构笔记(六): protobuf爬坑》,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对 码农网 的支持!
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。