Protobuf 主要是以数据编码小为著名,主要是用于数据交互和数据存储等,降低带宽、磁盘、移动端网络环境较差减少报文大小等场景,关于序列化速度主要是取决于你用的sdk,所以本文不会关心序列化速度!本文将以proto3语法进行介绍!并且也介绍了如何使用pb规范的定义接口,以及对比了pb2/pb3差别!如果你还对Thrift感兴趣,可以看我这边文章: Thrift协议讲解!
1. 协议讲解
- pb3 与 pb2差别:
- pb3 对于基本类型已经约定了默认值,会把
0
/""
/false
/枚举为0的值
在序列化的时候不进行编码,也就是无法区分这个值是否设置了! - pb3 后期支持了
optional
,但是需要在编译的时候指定--experimental_allow_proto3_optional
! - pb3 不支持
required
关键字,不推荐业务定义required
! - pb3 不支持默认值设置,pb3中默认值都是约定好的,以及不支持
group message
! - pb3 的枚举类型的第一个字段必须为 0!
- pb3 和 pb2 是可以混合使用的!pb3和pb2基本上压缩率和性能上无差别! | labels | pb2 | pb3 | 备注 | | ----------------------------------- | ------ | ------ | ---- | | required | 支持 | 不支持 | | | optional | 支持 | 支持 | | | singular (类似于thrift default) | 不支持 | 支持 | | | repeated | 支持 | 支持 | | | oneof | 支持 | 支持 | | | map | 支持 | 支持 | | | extend | 支持 | 不支持 | |
选择上来说就是看你是否需要 null
和默认值!如果需要那就pb2
,不行就pb3
!
- pb3基本上语法如下,具体可以看官方文档: developers.google.com/protocol-bu… , 例如下面的
test.proto
文件
syntax = "proto3";
message TestData {
enum EnumType {
UnknownType = 0; // 必须以0开始!
Test1Type = 1;
Test2Type = 2;
}
message TestObj {
int64 t_int64 = 1;
}
string t_string = 1;
int64 t_int64 = 2;
bool t_bool = 3;
fixed64 t_fix64 = 4;
repeated int64 t_list_i64 = 5;
map<int64, string> t_map = 6;
EnumType t_enum = 7;
TestObj t_obj = 8 ;
repeated TestObj t_list_obj = 9 ;
map<string, TestData> t_map_obj = 10;
repeated string t_list_string = 11;
}
- 如何编译了? 如果是Go的话可以下面这种方式编译!记住提前下载好
protoc-gen-go
和protoc-gen-go-grpc
, 源码地址: protobuf-go
# install protoc & protoc-gen-go & protoc-gen-go-grpc
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.17.3/protoc-3.17.3-osx-x86_64.zip
go get -v google.golang.org/protobuf/cmd/protoc-gen-go
go get -v google.golang.org/grpc/cmd/protoc-gen-go-grpc
# 编译上面的'test.proto'文件
protoc \
--experimental_allow_proto3_optional \
--proto_path=. \
--plugin=protoc-gen-go=${HOME}/go/bin/protoc-gen-go \
--go_opt=Mtest.proto=github.com/anthony-dong/go-tool/internal/example/protobuf/test \
--go_out=${HOME}/go/src \
--plugin=protoc-gen-go-grpc=${HOME}/go/bin/protoc-gen-go-grpc \
--go-grpc_opt=Mtest.proto=github.com/anthony-dong/go-tool/internal/example/protobuf/test \
--go-grpc_out=${HOME}/go/src \
test.proto
-
pb 序列化核心用到的思想就是varint + zigzap, 具体可以看官方文章: developers.google.com/protocol-bu…
-
本文的目标是可以做到简单的序列化 message 和 反序列化message!
2. 编码+解码
关于消息各个类型的编码逻辑: github.com/protocolbuf…
核心思想:
- varint,根据数字的大小进行动态编码,可以减少字节数的占用,采用 msb(the Most Significant Bit) 最高有效位,整个过程可以理解为大端->小端转换,具体可以看后面讲述!
- zigzag 由于负数的最高高位永远是1,导致-1占用8字节,比较浪费,所以zigzag做了一个映射,负数可以映射成正数,正数还是正数,具体可以看后面讲述!
1. 简单例子(学习目标)
下面是一个测试用例,可以看到一个是通过PB自带的库编码,一个是我们自己实现的编码,我们这篇文章目标是可以自己实现编码!
// 使用pb 序列化
func Test_Marshal_Data(t *testing.T) {
var request = test.TestData{
TString: "hello", // 1:string
TInt64: 520, //8:int64
TObj: &test.TestData_TestObj{ //8:message
TInt64: 520, // 1:int64
},
}
marshal, err := proto.Marshal(&request)
if err != nil {
t.Fatal(err)
}
t.Log(hex.Dump(marshal))
// 00000000 0a 05 68 65 6c 6c 6f 10 88 04 42 03 08 88 04 |..hello...B....|
}
// 自己编码完成!
func TestMarshal_Data_Custom_Test(t *testing.T) {
// 注释语法
// field_id:field_type:wire_type
// size(field_value)
// field_type=field_value
buffer := bytes.NewBuffer(make([]byte, 0, 1024))
binary.Write(buffer, binary.BigEndian, uint8(0x0a)) // 1:string:WireBytes, 0000 1010 = 0x0a
binary.Write(buffer, binary.BigEndian, uint8(0x05)) // size(string) = 5
binary.Write(buffer, binary.BigEndian, []byte("hello")) // string='hello', 68 65 6c 6c 6f
binary.Write(buffer, binary.BigEndian, uint8(0x10)) // 2:int64:WireVarint, 0001 0000 = 0x10
binary.Write(buffer, binary.BigEndian, uint16(0x8804)) // int64=520, 0000 0010 0000 1000 => 1000 1000 0000 0100 = 0x8804
binary.Write(buffer, binary.BigEndian, uint8(0x42)) // 8:message:WireBytes, 0100 0010 = 0x42
binary.Write(buffer, binary.BigEndian, uint8(0x03)) // size(message) = 3
binary.Write(buffer, binary.BigEndian, uint8(0x08)) // 1:int64:WireVarint, 0000 1000=0x08
binary.Write(buffer, binary.BigEndian, uint16(0x8804)) // int64=520, 0000 0010 0000 1000 => 1000 1000 0000 0100 = 0x8804
t.Log(hex.Dump(buffer.Bytes()))
// 00000000 0a 05 68 65 6c 6c 6f 10 88 04 42 03 08 88 04 |..hello...B....|
}
2. Message 编码介绍
1. 介绍
消息是由 field_id
和 field_value
组成,但是pb支持的类型比较多,考虑到编码的时候很多类型其实有相似的逻辑,因此pb对于类型进行了二次归类,叫做wire type
,也就是 field_id
和wire_type
组合成一个字段用varint
进行编码!
field id, wire_type used varint encode
+--------+...+--------+--------+--------+...+--------+
| field id |dddddttt| field value |
+--------+...+--------+--------+--------+...+--------+
field id
+dddddttt
一共是 1-4个字节,用的是 varint 编码!具体Go的代码实现如下
// EncodeTagAndWireType encodes the given field tag and wire type to the
// buffer. This combines the two values and then writes them as a varint.
func (b *Buffer) EncodeTagAndWireType(fieldId int32, wireType int8) error {
v := uint64((int64(fieldId) << 3) | int64(wireType))
return b.EncodeVarint(v)
}
// DecodeTagAndWireType decodes a field tag and wire type from input.
// This reads a varint and then extracts the two fields from the varint
// value read.
func (cb *Buffer) DecodeTagAndWireType() (tag int32, wireType int8, err error) {
var v uint64
v, err = cb.DecodeVarint()
if err != nil {
return
}
// low 7 bits is wire type
wireType = int8(v & 7)
// rest is int32 tag number
v = v >> 3
if v > math.MaxInt32 {
err = fmt.Errorf("tag number out of range: %d", v)
return
}
tag = int32(v)
return
}
ttt
为3bit表示wire_type
,也就是最多表示1<<3 -1
7种类型,包含000
,也就是8种类型,具体可以看官方文档: wire types 介绍!
const (
WireVarint = 0
WireFixed32 = 5
WireFixed64 = 1
WireBytes = 2
WireStartGroup = 3
WireEndGroup = 4
)
// 映射关系,就是 字段真实类型 -> 序列化类型
func MustWireType(t descriptor.FieldDescriptorProto_Type) int8 {
wireType, err := GetWireType(t)
if err != nil {
panic(err)
}
return wireType
}
func GetWireType(t descriptor.FieldDescriptorProto_Type) (int8, error) {
switch t {
case descriptor.FieldDescriptorProto_TYPE_ENUM,
descriptor.FieldDescriptorProto_TYPE_BOOL,
descriptor.FieldDescriptorProto_TYPE_INT32,
descriptor.FieldDescriptorProto_TYPE_SINT32,
descriptor.FieldDescriptorProto_TYPE_UINT32,
descriptor.FieldDescriptorProto_TYPE_INT64,
descriptor.FieldDescriptorProto_TYPE_SINT64,
descriptor.FieldDescriptorProto_TYPE_UINT64:
return proto.WireVarint, nil
case descriptor.FieldDescriptorProto_TYPE_FIXED32,
descriptor.FieldDescriptorProto_TYPE_SFIXED32,
descriptor.FieldDescriptorProto_TYPE_FLOAT:
return proto.WireFixed32, nil
case descriptor.FieldDescriptorProto_TYPE_FIXED64,
descriptor.FieldDescriptorProto_TYPE_SFIXED64,
descriptor.FieldDescriptorProto_TYPE_DOUBLE:
return proto.WireFixed64, nil
case descriptor.FieldDescriptorProto_TYPE_BYTES,
descriptor.FieldDescriptorProto_TYPE_STRING,
descriptor.FieldDescriptorProto_TYPE_MESSAGE:
return proto.WireBytes, nil
case descriptor.FieldDescriptorProto_TYPE_GROUP:
return proto.WireStartGroup, nil
default:
return 0, fmt.Errorf("not support pb type: %d", t)
}
}
field value
就是字段内容了,下面会详细介绍每一种对应的!
WireVarint 写的时候采用varint 编码,可变1-10字节
WireFixed32 写的时候会进行小端转换,固定4字节
WireFixed64 写的时候会进行小端转换,固定8字节
WireBytes 写的时候正常写出字节流即可!
WireStartGroup / WireEndGroup 不进行介绍了!
2. 总结
- 这里谈个小技巧,其实看协议编码这种源码的时候,很多位运算,其实一般来说
|
表示set bit操作,&
表示get bit操作! - 这里再补充下为啥最大字段是
2^29-1
,是因为nuber最大是ui32编码,然后有3bit用作msb,就剩余29位了,所以就是2^29-1
了! - 这就是为什么pb中
1-15字段
可以使用一个字节存储,是因为var int
只有7字段存储数据,但是3bit存储wire_type
,所以剩余的4bit
存储字段ID,也就是1<<4 -1 = 15
个字段了!
3. varint 编码介绍
wiki介绍 en.wikipedia.org/wiki/Variab… ,整体概述一下就是对于无符号整数
来说,很多时候都是浪费字节,比如uint64 占用 8字节,值为1是占用8字节,值为1<<64 - 1
也是一样,那么varint就是解决这个问题了,可以用1-10个字节进行表示!核心思想就是使用低位的7bit表示数据,高位1bit表示msb(The Most Significant Bit, 最高有效位),最小1个字节,最大10个字节表示 int64 !
pb中类型为如下类型都会采用varint
编码 , 枚举等同于int32
!
varint := int32 | int64 | uint32 | uint64 | bool | enum, encoded as varints
1. 例子1
比如: data=15 -> 0000 1111
,
编码逻辑:
varint 表示为 0000 1111
,是因为他能够用7字节表示!所以不需要设置 msb!
解析逻辑:
我们拿到 0000 1111
取出msb 发现1 ,这里拿到msb有多种方式,可以比较大小,也能通过位运算进行取,比如 0000 1111 & 1<<7 == 0
就可以说明没有设置msb,然后取出低7位即是真实数据,这里由于8位也是0其实可以忽略这个操作!
2. 例子2
比如 data=520 -> 0000 0010 0000 1000
(大端表示法,低位在高地址)
编码逻辑:
首先确定520
是7个bit放不下,所以先取出 前7个字节( data & (1<<7) - 1)
= 000 1000
,然后设置msb 1000 1000
, 这个是第一轮;
第二轮剩余字节 0000 0010 0
= 4
, 发现4
可以用7个字节放下,所以是 0000 0100
所以最终结果是 1000 1000 0000 0100
,也就是 [136,4],这个过程可以理解为是大端 -> 小端的一个过程!
解析逻辑:
首先varint 其实输出的是一个小端表示法,因此我们需要从低位开始!
首先是取出第一个字节1000 1000
,发现msb,然后得到结果是 000 1000
= 8
然后是取出第二个字节0000 0100
,发现不是msb,然后得到结果 000 0100
,我们需要将它放到 000 1000
后面去!怎么做了,其实跟简单 000 0100 << 7 | 000 1000
即可得到结果是 000 0100 000 1000
= 0000 0010 0000 1000
。 这个逻辑可以理解为是小端->大端的一个过程
3. 代码实现
func (p *pbCodec) EncodeVarInt(data int64) error {
// 1. 取出低7位(如果7个字节不可以放下!)
// 2. 然后设置高8位标识符号
// 3. 然后右移
for data > (1<<7 - 1) {
p.buffer = append(p.buffer, byte(data&(1<<7-1)|(1<<7)))
data >>= 7
}
p.buffer = append(p.buffer, byte(data))
return nil
}
func (p *pbCodec) DecodeVarInt() (int64, error) {
var (
x int64
n = 0
)
defer func() {
p.buffer = p.buffer[n:]
}()
for shift := uint(0); shift < 64; shift += 7 { // 偏移量从0开始,每次+7
if n >= len(p.buffer) {
return 0, fmt.Errorf("not enough buffer")
}
// 1. 取出第一个自己
// 2. 然后取出低7位
// 3. 然后由于数据是小端,所以取出的数据需要移动偏移量
// 4. 然后设置进去原来的数据中!
b := int64(p.buffer[n])
n++
x |= (b & 0x7F) << shift
if (b & 0x80) == 0 {
return x, nil
}
}
return 0, fmt.Errorf("proto integer overflow")
}
4. 非 varint 编码类型
1. fixed 64/32 类型 (小端)
其实就是用小端进行传输!例如fixed64 = 520
小端编码后如下,为此为了和varint进行区分,所以定了两个wire type=WireFixed32|WireFixed64
# fix64 520 占用 8 字节
00 00 00 00 00 00 02 08
# 编码后
08 02 00 00 00 00 00 00
例如Go代码的具体实现, 这里以 64
位为例子
import "encoding/binary"
// 写的时候可以通过如下
binary.Write(bf, binary.LittleEndian, uint64(520))
// 读的时候可以通过如下实现
var data uint64
binary.Read(bf, binary.LittleEndian, &data)
2. double / float 类型
同上面的fixed 64/32
,double需要转换为 fixed64
, float需要转换为fixed32
, 具体 float -> uint
Go的转换代码实现:
import "math"
math.Float32bits(v)
math.Float64bits(v)
3. string / bytes / message / packed 类型
string 和 bytes 都是变长,所以需要先写长度(var int)编码,再写payload,如果是string的话需要utf-8编码!
message 类型也是采用的如下编码,所以在PB里无法通过二进制报文直接解析!
delimited := size (message | string | bytes | packed), size encoded as varint
message := valid protobuf sub-message
string := valid UTF-8 string (often simply ASCII); max 2GB of bytes
bytes := any sequence of 8-bit bytes; max 2GB
delimited := size (message | string | bytes | packed), size encoded as varint
# size bytes
+--------+...+--------+--------+...+--------+
| byte length | bytes |
+--------+...+--------+--------+...+--------+
5. zigzag 编码 ( sint32 / sint64)
前面讲的varint并不是万能的,因为数据往存在负数,而负数二进制最高位都是1,所以导致varint编码后数据都很大,所以需要zigzag
编码,它可以帮负数转换成正数,正数转换成正数!而且基于位运算效率很高,所以pb提出了sint32
、sint64
编码,解决这个问题,核心其实就是使用了 zigzag
编码!
例如: int64 类型,值为 -1, varint 编码是:ff ff ff ff ff ff ff ff ff 01
满满的占用了10个字节! 但是假如是 sint64 类型,值为 -1, zigzag 编码后值为01
,然后varint编码后是 01
, 此时就节省了9个字节!
zigzag 编码其实很简单就是类似于做了层映射!用无符号的一半表示正数一半表示负数!
具体算法用Go写大改如下:
// EncodeZigZag64 does zig-zag encoding to convert the given
// signed 64-bit integer into a form that can be expressed
// efficiently as a varint, even for negative values.
func EncodeZigZag64(v int64) uint64 {
return (uint64(v) << 1) ^ uint64(v>>63)
}
// EncodeZigZag32 does zig-zag encoding to convert the given
// signed 32-bit integer into a form that can be expressed
// efficiently as a varint, even for negative values.
func EncodeZigZag32(v int32) uint64 {
return uint64((uint32(v) << 1) ^ uint32((v >> 31)))
}
// DecodeZigZag32 decodes a signed 32-bit integer from the given
// zig-zag encoded value.
func DecodeZigZag32(v uint64) int32 {
return int32((uint32(v) >> 1) ^ uint32((int32(v&1)<<31)>>31))
}
// DecodeZigZag64 decodes a signed 64-bit integer from the given
// zig-zag encoded value.
func DecodeZigZag64(v uint64) int64 {
return int64((v >> 1) ^ uint64((int64(v&1)<<63)>>63))
}
异或:相同为0,相异为1
例如下面例子,将-1
和 1
进行zigzag 编码后:
# -1
1111 1111 1111 1111 1111 1111 1111 1111
# d1=uint32(n) << 1
1111 1111 1111 1111 1111 1111 1111 1110
# d2=uint32(n >> 31) (负数左移添加1)
1111 1111 1111 1111 1111 1111 1111 1111
# d1 ^ d2
0000 0000 0000 0000 0000 0000 0000 0001
# 1
0000 0000 0000 0000 0000 0000 0000 0001
#n<<1
0000 0000 0000 0000 0000 0000 0000 0010
#n>>31
0000 0000 0000 0000 0000 0000 0000 0000
# 输出
0000 0000 0000 0000 0000 0000 0000 0010
6. repeated (list)
上文都没有讲解到 集合类型,protbuf 提供了 repeated关键字来提供list类型!关于 repeated 具体编码实现有两种:
-
packed ( pb3默认会根据字段类型选择packed, pb2 v2.1.0 引入的,具体可以参考官方文档: packed介绍 )
-
unpacked
目前pb中支持 wire_type=WireVarint|WireFixed32|WireFixed64
进行 packed
编码!
其实可以思考一下为啥!首先假如是WireBytes
类型,那么我数据量很大,比如一个bytes
数据最大可以写2G,那么我写出的时候假如用packed
编码,会存在一个问题就是我写3条数据,需要内存中积压6G数据,然后算出总大小,再写出去,占用内存很大,而且解码的时候也是!PB考虑的可真细致!
1. packed 编码
可以根据官网提供的demo为例子:
message Test4 {
repeated int32 d = 4 [packed=true];
}
假如d= [3, 270,86942]
,编码d字段的时候,会进行如下操作,先写 field_number
和 wire_type
然后再去写整个payload 大小,最后再写每一个元素!
22 // key (field number = 4, wire type = 2 WireBytes)
06 // payload size (6 bytes)
03 // first element (varint 3)
8E 02 // second element (varint 270)
9E A7 05 // third element (varint 86942)
2. unpacked 编码
message Test5 {
repeated int32 d = 4 [packed = false];
}
还是以字段d= [3, 270,86942]
进行编码,可以看到是会把每个元素作为单独的整体进行写出,比如元素一会写field_type and wire_type
,然后再写 field_value
,依次!!
00000000 20 03 20 8e 02 20 9e a7 05 | . .. ...|
20 // key (field number 4, wire type=proto.WireVarint)
03 // (varint 3)
20 // key (field number 4, wire type=proto.WireVarint)
8e 02 // (varint 270)
20 // key (field number 4, wire type=proto.WireVarint)
9e a7 05 // (varint 86942)
7. map
其实在PB中map实际上就是 repeated kv message,可以通过FieldDescriptor
可以看到!
{
"name": "TestData",
"field": [
{
"name": "t_map",
"number": 6,
"label": 3,
"type": 11,
"type_name": ".TestData.TMapEntry",
"json_name": "tMap"
},
],
"nested_type": [
{
"name": "TMapEntry",
"field": [
{
"name": "key",
"number": 1,
"label": 1,
"type": 3,
"json_name": "key"
},
{
"name": "value",
"number": 2,
"label": 1,
"type": 9,
"json_name": "value"
}
],
"options": {
"map_entry": true
}
}
],
"enum_type": []
}
所以编码的时候也很简单,例如
message TestMapData1 {
// 这里无法定义 TMapEntry,会报错!
map<int64, string> t_map = 6;
}
==> 实际上是生成了这个代码!
message TestMapData2 {
message TMapEntry {
int64 key = 1;
string value = 2;
}
repeated TMapEntry t_map = 6;
}
所以编码过程是一个repeated k v message的序列化方式!例如下面
t_map= {1:"1",2:"2"}
=>
32 05 08 01 12 01 31 32 05 08 02 12 01 32
=>
32 // field_number=6 and wire_type=proto.WireBytes
05 // entry data length=5
08 // entry data key field_number=1 and wire_type=proto.WireVarint
01 // entry data key_value=varint(1)
12 // entry data value field_number=2 and wire_type=proto.WireBytes
01 // entry data value len= varint(1)
31 // entry data value="1"
32 // field_number=6 and wire_type=proto.WireBytes
05 // entry data length=5
08 02 12 01 32 // 同上!
8. field order
pb编码不在意字段的编码顺序,也就是encode的字段顺序不一样会导致输出的数据不一样!但是解析出来的数据是一样的!
还有就是map的key顺序也会影响!
所以一般api都会指定是否支持 deterministic
,如果设置为true,结果一般都会保证一样,否则可能不一样!
但是你懂得,实际上效果吧,就是开启之后一定比不开启慢,因为需要进行order!
3. pb 协议整体概括
下面这个是一个类似于bnf范式的东西,具体可以参考: PB Encode 算法
message := (tag value)* You can think of this as “key value”
tag := (field << 3) BIT_OR wire_type, encoded as varint
value := (varint|zigzag) for wire_type==0 |
fixed32bit for wire_type==5 |
fixed64bit for wire_type==1 |
delimited for wire_type==2 |
group_start for wire_type==3 | This is like “open parenthesis”
group_end for wire_type==4 This is like “close parenthesis”
varint := int32 | int64 | uint32 | uint64 | bool | enum, encoded as
varints
zigzag := sint32 | sint64, encoded as zig-zag varints
fixed32bit := sfixed32 | fixed32 | float, encoded as 4-byte little-endian;
memcpy of the equivalent C types (u?int32_t, float)
fixed64bit := sfixed64 | fixed64 | double, encoded as 8-byte little-endian;
memcpy of the equivalent C types (u?int64_t, double)
delimited := size (message | string | bytes | packed), size encoded as varint
message := valid protobuf sub-message
string := valid UTF-8 string (often simply ASCII); max 2GB of bytes
bytes := any sequence of 8-bit bytes; max 2GB
packed := varint* | fixed32bit* | fixed64bit*,
consecutive values of the type described in the protocol definition
varint encoding: sets MSB of 8-bit byte to indicate “no more bytes”
zigzag encoding: sint32 and sint64 types use zigzag encoding.
4. protoc 命令讲解
这里讲解一下protoc 的架构图,生成架构图
第一个节点protoc
其实是 c++写的, github.com/protocolbuf…
第二个节点是 protoc
输出的二进制报文CodeGeneratorRequest
,具体可以看 CodeGeneratorRequest IDL定义
第三个节点是plugin生成逻辑,可以通过标准输入获取CodeGeneratorRequest
,通过标准输出写入CodeGeneratorResponse
第四个节点输出 CodeGeneratorResponse
,具体可以看 CodeGeneratorResponse IDL定义
目前虽然有很多项目在解析protoc 的 ast时,都是自己实现的词法解析和语法解析,所以假如可以把protoc封装个lib库就好了!
5. pb 其他细节讲解
- package(包),一个包下不能存在相同定义的message和枚举以及枚举字段!
一般来说定义规则是
common.prefix
+文件路径
, 其实和Java的Package定义很像!这个是比较推荐的方案!
-
include(import),基于include_path 作为根目录的relative pata
-
option,可以理解为是一些注解,可以对于字段、消息、枚举、method进行标记!但是必须注解定义文件必须使用
pb2
语法!
这里不推荐在idl里定义
option go_package=xxx java php py
之类的,因为pb在编译期可以指定!如果你们有自己的pb gen plugin可以拓展descriptor
!
syntax = "proto2";
package api;
option go_package = "github.com/anthony-dong/go-tool/internal/example/protobuf/idl_example/pb_gen/api";
import "google/protobuf/descriptor.proto";
extend google.protobuf.FieldOptions {
optional HttpSourceType source = 50101; // 来自于http 请求的哪个部位
optional string key = 50102; // http 请求的header 还是哪
optional bool unbox = 50103; // 是否平铺开结构体,除body外,默认处理第一层
}
enum HttpSourceType {
Query = 1;
Body = 2;
Header = 3;
}
extend google.protobuf.MethodOptions {
optional HttpMethodType method = 50201; // http method
optional string path = 50202; // http path
}
enum HttpMethodType{
GET = 1;
POST = 2;
PUT = 3;
}
// extend google.protobuf.EnumValueOptions {
// }
// extend google.protobuf.EnumOptions {
// }
// extend google.protobuf.MessageOptions {
// }
// extend google.protobuf.ServiceOptions {
// }
具体可以看我的写的例子:protobuf