文章

protobuf协议

Protobuf

Protobuf 提供了一种紧凑的二进制格式,序列化后的数据体积小,解析速度快,适合高性能场景。它支持多种编程语言,包括 C++、Java、Python、Go 等,能够实现跨语言的数据交换。

Protobuf 常用于以下场景:

  • 分布式系统:在服务之间传递高效的结构化数据。

  • RPC 框架:与 gRPC 集成,用于定义服务接口和消息格式。

  • 数据存储:用于存储需要高效读取和写入的结构化数据。

三种序列化协议的差异

 XMLJSONProtobuf
数据存储文本文本二进制
序列化存储消耗较大小(XML的1/3~1/10
序列化/反序列化速度快(XML的20-100倍)
数据类型支持广泛的数据类型支持基本的数据类型需要通过message定义来指定数据类型
跨平台支持支持支持支持

再来看一个小例子。我们需要传输一个结构体类型的数据,结构体如下:

1
2
3
4
struct Student {
	int id;
	std::string name;
}

使用XML序列化:

1
2
3
4
 <student>
   <id>101</id>
   <name>hello</name>
 </student>

使用json序列化:

1
2
3
4
 {
  "id": 101,
  "name": "hello"
 }

使用Protobuf二进制序列化:

1
 08 65 12 06 48 65 6C 6C 6F 77

语法

message 的定义语法:

1
2
3
4
5
<comment>
message  <message_name> {
  <filed_rule>  <filed_type> <filed_name> = <field_number> 
       规则          类型          名称           编号  
}
  • comment: 注射 /* */或者 //
  • message_name: 同一个pkg内,必须唯一
  • filed_rule: 可以没有, 常用的有repeated, oneof
  • filed_type: 数据类型, protobuf定义的数据类型, 生产代码的会映射成对应语言的数据类型
  • filed_name: 字段名称, 同一个message 内必须唯一
  • field_number: 字段的编号, 序列化成二进制数据时的字段编号

字段编号规则

  • 必须为正整数,范围 1 ~ 2^29-119000~19999 保留给 protobuf 实现)。
  • 不能重复,不能随意变更(已上线字段只能 deprecate / reserve)。
  • 可以乱序
  • 1-15 占 1 byte,16-2047占 2 byte;高频字段尽量用 1-15。

示例

1
2
3
4
5
message Demo {
  int32 a = 9;
  int32 b = 2;   // 编号 2 比 9 小,完全 OK
  int32 c = 200; // 跳得再大也没问题
}

值类型

消息字段可以具有以下类型之一:

img

示例

1
2
3
4
5
6
7
8
message ActionInfo {
  // 当前毫秒时间戳
  int64 timestamp = 0;
  // 内部生成的 ID
  string action_id = 1;
  // 是否只能客户端发送
  bool only_client = 2;
}

枚举类型

枚举声明语法:

1
2
3
enum <enum_name> {
    <element_name> = <element_number>
}
  • enum_name: 枚举名称
  • element_name: pkg内全局唯一, 很重要
  • element_number: 必须从0开始, 0表示类型的默认值, 32-bit integer

示例

1
2
3
4
5
6
7
8
9
enum Corpus {
    UNIVERSAL = 0;
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
}

别名

如果的确有2个同名的枚举需求: 比如 TaskStatus 和 PipelineStatus 都需要Running,就可以添加一个: option allow_alias = true;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
message MyMessage1 {
  enum EnumAllowingAlias {
    option allow_alias = true;
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
  }
}
message MyMessage2 {
  enum EnumNotAllowingAlias {
    UNKNOWN = 0;
    STARTED = 1;
    // RUNNING = 1;  // Uncommenting this line will cause a compile error inside Google and a warning message outside.
  }
}

预留值

可以使用max关键字指定保留的数值范围达到最大可能值。

1
2
3
4
5
6
7
8
enum Foo {
  UNIVERSAL = 0;
  WEB = 1;
  // IMAGES = 2; //Enum value 'IMAGES' uses reserved number 2
  YOUTUBE = 3; 
  reserved 2, 15, 9 to 11, 40 to max;
  reserved "FOO", "BAR";
}

数组类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
message  Result {
  int32 age = 1;
  string name = 2;
}

message SearchResponse {
  repeated Result result = 1;  // 数组
}

// protoc -I=./ --go_out=./pbrpc/service --go_opt=module="github.com/ProtoDemo/pbrpc/service" pbrpc/service/test.proto
// 会编译为:
//type Result struct {
// ... ...
//	Age  int32  `protobuf:"varint,1,opt,name=age,proto3" json:"age,omitempty"`
//	Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
//}

//type SearchResponse struct {
// ... ...
//	Result []*Result `protobuf:"bytes,1,rep,name=result,proto3" json:"result,omitempty"`
//}

Map

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
message Project {
    int32 age = 1;
    string name = 2;
}

message MapData {
  map<string, Project> projects = 1;  // 字典
}

// protoc -I=./ --go_out=./pbrpc/service --go_opt=module="github.com/ProtoDemo/pbrpc/service" pbrpc/service/test.proto
// projects map[string, Project]
// 会编译为:
//type Project struct {
// ... ...
//	Age  int32  `protobuf:"varint,1,opt,name=age,proto3" json:"age,omitempty"`
//	Name string `protobuf:"bytes,2,opt,name=name,proto3" json:"name,omitempty"`
//}

//type MapData struct {
// ... ...
//	Projects map[string]*Project `protobuf:"bytes,1,rep,name=projects,proto3" json:"projects,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
//}

Oneof

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
message Sub1 {
    string name = 1;
}

message Sub2 {
    string name = 1;
}

message SampleMessage {   
    oneof test_oneof {    // 互斥,最多选择一个
        Sub1 sub1 = 1;
        Sub2 sub2 = 2;
    }
}

// protoc -I=./ --go_out=./pbrpc/service --go_opt=module="github.com/ProtoDemo/pbrpc/service" pbrpc/service/test.proto
// 会编译为:
//type SampleMessage struct {
//	state         protoimpl.MessageState
//	sizeCache     protoimpl.SizeCache
//	unknownFields protoimpl.UnknownFields
//
//	// Types that are assignable to TestOneof:
//	//	*SampleMessage_Sub1
//	//	*SampleMessage_Sub2
//	TestOneof isSampleMessage_TestOneof `protobuf_oneof:"test_oneof"`
//}

// 操作使用
// of := &pb.SampleMessage{}
// of.GetSub1()
// of.GetSub2()

示例

1
2
3
4
5
6
7
oneof data {
    SendMsgAction send_msg_action = 4;
    TransferAction transfer_action = 5;
    ClientNotifyAction client_notify = 6;
    LightAction light_action = 7;
    SatisfactionInviteAction satisfaction_invite_action = 11;
}

生成对应go代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//Go 生成代码
type ActionInfo struct {
    // 内部是一个接口,只可能指向其中一个具体类型
    Data isActionInfo_Data `protobuf_oneof:"data"`
}

type isActionInfo_Data interface {
    isActionInfo_Data()
}

type ActionInfo_SendMsgAction struct {
    SendMsgAction *SendMsgAction
}
type ActionInfo_TransferAction struct {
    TransferAction *TransferAction
}
// ... 其他分支

//使用:
switch x := msg.Data.(type) {
case *ActionInfo_SendMsgAction:
    handleSend(x.SendMsgAction)
case *ActionInfo_TransferAction:
    handleTransfer(x.TransferAction)
default:
    // 没设置或未知
}

Any

当无法明确定义数据类型的时候, 可以使用Any表示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  repeated google.protobuf.Any details = 2  //无法明确定义数据类型;
}


// protoc -I=./ -I=/usr/local/include --go_out=./pbrpc/service --go_opt=module="github.com/ProtoDemo/pbrpc/service" pbrpc/service/test.proto
// 会编译为:
// any本质上就是一个bytes数据结构
//type ErrorStatus struct {
//	state         protoimpl.MessageState
//	sizeCache     protoimpl.SizeCache
//	unknownFields protoimpl.UnknownFields
//
//	Message string       `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
//	Details []*anypb.Any `protobuf:"bytes,2,rep,name=details,proto3" json:"details,omitempty"`
//}

类型嵌套

可以再message里面嵌套message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message Outer {                  // Level 0
  message MiddleAA {  // Level 1
    message Inner {   // Level 2
      int64 ival = 1;
      bool  booly = 2;
    }
  }
  message MiddleBB {  // Level 1
    message Inner {   // Level 2
      int32 ival = 1;
      bool  booly = 2;
    }
  }
}

与Go结构体嵌套一样, 但是不允许 匿名嵌套, 必须指定字段名称

默认值

反序列化消息时,如果被反序列化的二进制序列中不包含某个字段,反序列化对象中相应字段时,就会设置为该字段的默认值。不同的类型对应的默认值不同:

  • 对于 字符串,默认值为空字符串
  • 对于 字节,默认值为空字节
  • 对于 布尔值,默认值为 false
  • 对于 数值类型,默认值为 0
  • 对于 消息字段,未设置该字段。它的取值是依赖于语言
  • 对于 设置了 repeated 的字段 的默认值是空的( 通常是相应语言的一个空列表 )
  • 对于 消息字段 、 oneof 字段 和 any 字段C++Java 语言中都有 has_ 方法来检测当前字段是否被设置;而对于 标量数据类型,在 proto3 语法下是不会生成 has_ 方法的!

用于 HTTP /gRPC

案例

MsgAnswerSend 的签名要求传入 *msgAdapterPb.SendMsgByAction,无论 HTTP 还是 gRPC/Protobuf,核心都是构造这份结构体:

func (d *MsgAdapterHandler) SendMsgByActionHttp(c *gin.Context) {
    ...
    req := &msgAdapterPb.SendMsgByAction{}
    body, _ := io.ReadAll(c.Request.Body)
    _ = protojson.Unmarshal(body, req)
    msgAdapter := msg_adapter.GetMsgAdapter(req.Platform, model.MsgSourceHttp)
    err = msgAdapter.MsgAnswerSend(ctx, req)
    ...
}
func (d *MsgAdapterHandler) SendMsg(ctx context.Context, req *msgAdapterPb.SendMsgByAction) (*msgAdapterPb.SendMsgByActionRsp, error) {
    msgAdapter := msg_adapter.GetMsgAdapter(req.Platform, model.MsgSourceHttp)
    err := msgAdapter.MsgAnswerSend(ctx, req)
    ...
}

HTTP 传参示例

  • 调用地址:POST /adapter/send_msg
  • Header:Content-Type: application/json(使用 protojson 编码)
  • Body(示例字段含义:平台、会话双方、动作列表等):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
POST /adapter/send_msg HTTP/1.1
Host: your-host
Content-Type: application/json

{
  "platform": "jd",
  "shopId": "6874d0feb8a8af86499db200",
  "fromPin": "jimi_vender_22397354",
  "toPin": "jd_vnyjzwhanyyt",
  "msgId": "f1c1df56f6a9496b8f7b1bd13a6992b3",
  "requestId": "a2b90ccb5a3d42c2a6c1a761b5cb2f38",
  "actionList": [
    {
      "actionType": "ACTION_TYPE_SEND_MSG",
      "actionId": "act_1732778149123",
      "timestamp": 1732778149,
      "sendMsgAction": {
        "msgInfo": {
          "msgType": "SEND_MSG_TYPE_TEXT",
          "text": {
            "content": "亲,您好~"
          }
        },
        "delay": 0,
        "confirm": false
      }
    }
  ]
}

actionList 内使用具体的 SendMsgActionTransferAction 等子结构,字段名与 proto 定义一致(驼峰形式)。如果要带扩展信息,可以补充 extraDatabusiness 等字段。

Protobuf/gRPC 传参示例

通过 gRPC 调用 AppBusinessToAdapterService.SendMsg,直接传送二进制 Protobuf:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
package main

import (
    "context"
    "time"

    msgAdapterPb "gitlab.xiaoduoai.com/base/xdproto/common_business/msg_adapter/pb"
    "google.golang.org/grpc"
)

func main() {
    conn, _ := grpc.Dial("adapter-service:50051", grpc.WithInsecure())
    defer conn.Close()

    client := msgAdapterPb.NewAppBusinessToAdapterServiceClient(conn)
    ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
    defer cancel()

    req := &msgAdapterPb.SendMsgByAction{
        Platform: "jd",
        ShopId:   "6874d0feb8a8af86499db200",
        FromPin:  "jimi_vender_22397354",
        ToPin:    "jd_vnyjzwhanyyt",
        MsgId:    "f1c1df56f6a9496b8f7b1bd13a6992b3",
        ActionList: []*msgAdapterPb.ActionInfo{
            {
                ActionType: msgAdapterPb.ActionType_ACTION_TYPE_SEND_MSG,
                ActionId:   "act_1732778149123",
                Timestamp:  time.Now().Unix(),
                Data: &msgAdapterPb.ActionInfo_SendMsgAction{
                    SendMsgAction: &msgAdapterPb.SendMsgAction{
                        MsgInfo: &msgAdapterPb.SendMsgInfo{
                            MsgType: msgAdapterPb.SendMsgType_SEND_MSG_TYPE_TEXT,
                            Data: &msgAdapterPb.SendMsgInfo_Text{
                                Text: &msgAdapterPb.MessageFormatText{Content: "亲,您好~"},
                            },
                        },
                    },
                },
            },
        },
    }

    _, err := client.SendMsg(ctx, req)
    if err != nil {
        panic(err)
    }
}

关键点:

  • HTTP 入口以 protojson 解析,字段名需与 proto 中的 json tag 对齐。
  • gRPC 入口直接接收 Protobuf 对象,字段名用 Go 生成的驼峰属性即可。
  • 二者最终都会调用 MsgAnswerSend(ctx, req),所以 SendMsgByAction 数据结构保持一致即可。

解析的流程

步骤HTTP 方式gRPC 方式
数据格式JSON 字符串Protobuf 二进制
读取数据手动 io.ReadAll(c.Request.Body)gRPC 框架自动从流中读取
解析数据手动 protojson.Unmarshal(body, req)gRPC 框架自动调用 req.Unmarshal()
解析位置在你的 handler 代码中(第 60 行)在 gRPC 框架内部(google.golang.org/grpc 库)
Handler 接收需要手动解析后使用直接接收已解析的结构体

解析库

json 和 protojson使用

不是直接使用encoding/json库的原因:

序列化器能否用于 Protobuf GO 结构体输出标准 Protobuf JSON能正确处理特殊 proto 类型
encoding/json可以,但有限
protojson.Marshal可以

解析区别

1
MsgId string `protobuf:"bytes,1,opt,name=msg_id,json=msgId,proto3"  json:"msg_id,omitempty"`
使用场景使用的标签使用字段示例
protojson + UseProtoNames: trueprotobuf 标签中的name=msg_id“msg_id”{“msg_id”: “123”}
protojson + UseProtoNames: falseprotobuf 标签中的 json=msgId“msgId”{“msgId”: “123”}
encoding/jsonjson:”msg_id” 标签“msg_id”{“msg_id”: “123”}

protojson 库

相关的包:

1
google.golang.org/protobuf/encoding/protojson

语法

1
2
3
4
5
//
defaultOpts := protojson.MarshalOptions{}
data2, _ := defaultOpts.Marshal(待解析)
// 
protojson.Unmarshal([]byte(待解析proto数据), &结果)

案例

protobuf 定义

1
2
3
4
5
6
7
8
9
10
11
12
message ActionInfo {
    string action_id = 1;        // protobuf 字段名:action_id
    bool only_client = 10;        // protobuf 字段名:only_client
    ActionType action_type = 5;  // 枚举类型
}

enum ActionType {
  // 未指定的动作类型
  ACTION_TYPE_UNSPECIFIED = 0;
  // 发送消息动作
  ACTION_TYPE_SEND_MSG = 1;
}

生成的go代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
type ActionInfo struct {
	state         protoimpl.MessageState
	sizeCache     protoimpl.SizeCache
	unknownFields protoimpl.UnknownFields

	// 动作类型
	ActionType ActionType `protobuf:"varint,1,opt,name=action_type,json=actionType,proto3,enum=xdproto.common_business.msg_adapter.ActionType" json:"action_type,omitempty"`
	// 内部生成的 ID
	ActionId string `protobuf:"bytes,9,opt,name=action_id,json=actionId,proto3" json:"action_id,omitempty"`
	// 是否只能客户端发送
	OnlyClient bool `protobuf:"varint,10,opt,name=only_client,json=onlyClient,proto3" json:"only_client,omitempty"`
}

type ActionType int32
const (
	// 未指定的动作类型
	ActionType_ACTION_TYPE_UNSPECIFIED ActionType = 0
	// 发送消息动作
	ActionType_ACTION_TYPE_SEND_MSG ActionType = 1
)

使用配置(UseProtoNames: true, UseEnumNumbers: true)

var (
	marshalOpts = protojson.MarshalOptions{
		UseProtoNames:  true,
		UseEnumNumbers: true,
	}
)

序列化结果:

1
2
3
4
5
"action_info": {                 //  使用 protobuf 原始字段名 action_info
    "action_id": "action_456",      //  嵌套的 ActionInfo 也使用 action_id
    "only_client": true,          //  使用 protobuf 原始字段名 only_client
    "action_type": 1             //  枚举值使用数字 1(不是字符串)
}

使用默认配置(UseProtoNames: false, UseEnumNumbers: false)

1
2
3
// 默认配置
defaultOpts := protojson.MarshalOptions{}
data, _ := defaultOpts.Marshal(callBackMsg)

序列化结果:

1
2
3
4
5
"actionInfo": {                   //  使用 JSON 命名(驼峰式)
    "actionId": "action_456",        //  嵌套的 ActionInfo 也使用 actionId
    "onlyClient": true,            //  使用 JSON 命名(驼峰式)
    "actionType": "ACTION_TYPE_TEXT"   //  枚举值使用字符串名称
} 

总结

image-20251129175112804

© 2024- lfj