Protocol Buffer是Google的语言中立的,平台中立的,可扩展机制的,用于序列化结构化数据 - 对比XML,但更小,更快,更简单。您能够定义数据的结构化,而后可使用特殊生成的源代码轻松地在各类数据流中使用各类语言编写和读取结构化数据。python
主要有点有:git
1.protoBuf在Google内部长期使用,产品稳定成熟,不少商业的项目都选择使用github
2.跨语言,它支持Java、C++、Python、ObJect-c、C#、Go等语言golang
3.protoBuf编码后消息更小、有利于存储传输web
4.编码和解码的效率很是之高shell
5.支持不一样版本的协议向前兼容npm
我使用的proto版本是protobuf3,关于proto的学习网络上已有许多优秀的文章,在这再也不赘述。 本文只介绍我在使用protobuf过程当中收获的经验和遇到的坑以及如何解决的。json
protobuf 语法指南gulp
若是要在多个项目中共用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文件,按需索取便可
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=../../../
, 这一点能够根据本身状况定制
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
复制代码
在定义双向流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,所以一直在使用中
默认状况下,当须要将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 写死的... 解决办法有
m := jsonpb.Marshaler{EmitDefaults: true}
复制代码
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等知识的时间较短,若是有理解错误的地方,欢迎批评指正,能够加我微信一块儿探讨学习