Protobuf 如何确定消息解析类

在使用 Protocol Buffers(Protobuf)时,如果你接收到的数据类型不确定,即你不知道该数据应该对应哪一个 .proto 文件定义的消息结构,那么你需要一种方法来区分这些不同的消息类型。以下是几种处理这种不确定性的常见策略,以 PHP 语言为例:

方法一 :使用 type 字段

一个常见的做法是在你的 Protobuf 消息中添加一个字段来表示消息的类型。例如:

message WrapperMessage {
    string type = 1;
    google.protobuf.Any data = 2;
}

然后你可以为每种可能的消息类型定义单独的消息,并且在序列化之前将正确的类型信息和数据放入 WrapperMessage 中。

在接收端,你可以先解析出 WrapperMessage,检查 type 字段,然后根据这个字段选择合适的消息类型进行进一步的反序列化。

方法二:封装在一个外层消息中

  • 设计一个外层消息,其中包含一个 oneof 字段(如果 protobuf 版本支持,或通过类似逻辑模拟),该字段可以包含多个可能的内部消息类型。

  • 在解析时,先解析外层消息,然后根据 oneof 的选择解析内部消息。

syntax = "proto3";
package c2s_common;

// BaseMessage 是最终发送的消息, 接收到后动态解析
message BaseMessage {
  oneof Message {
    Ping ping = 1;
    EnterGame enter_game = 2;
    LeaveGame leave_game = 3;
  }
}

// 客户端发送ping
message Ping {
  string topic = 1;
}

// 进入游戏上报
message EnterGame {
  string topic = 1;
  uint32 room_id = 2 [json_name = "room_id"];
}

// 离开游戏上报
message LeaveGame {
  string topic = 1;
  uint32 room_id = 2 [json_name = "room_id"];
}
  • PHP 代码实现
public static function parseProtobuf2Json($string): string
    {
        try {
            // 反序列化
            $baseMessage = new BaseMessage();
            $baseMessage->mergeFromString($string);
            $data = match ($baseMessage->getMessage()) {
                'ping' => $baseMessage->getPing(),
                'enter_game' => $baseMessage->getEnterGame(),
                'leave_game' => $baseMessage->getLeaveGame(),
                default => throw new \Exception('unknown message type'),
            };
            return $data->serializeToJsonString();
        } catch (\Exception $e) {
            SelfWorker::Log($e->getMessage());
            SelfWorker::Log($e->getTraceAsString());
            return '{}';
        }
    }

推荐

  • 如果消息类型较多且需要频繁扩展:推荐使用 方法1(使用字段标识符)。这种方法更灵活,可以轻松扩展新的消息类型。

  • 如果消息结构相对固定且需要减少冗余:推荐使用 方法2(封装在一个外层消息中)。这种方法可以减少冗余并自动处理解析逻辑,使代码更简洁。