前阵子做的一个直播弹幕的机器人,其中有一部分上游数据是通过 Protobuf 返回的。几个朋友问我怎么处理,但我发现大家对「PHP 解析 Protobuf」这件事多少有点迷糊。确实,PHP 处理 Protobuf 的资料不多,而且踩坑成本不算低。

这篇文章不打算科普什么,也没有推荐任何技术栈的意思,就是把我自己摸索的过程整理出来,给遇到类似问题的人一个参考。


Protobuf 是什么

很多人第一次接触它时,会把它和 JSON、XML 放在一起理解,但 Protobuf 并不是“另一个 JSON”。它是一种 ​基于 Schema 的二进制数据格式,本质上由两个部分组成:

  • .proto:数据结构的描述文件(类似字典)
  • 二进制格式:根据 .proto 规则编码出来的数据

Google 发明它的原因大致是:

  • JSON 太大、太慢
  • 在高性能、跨语言通信场景里不够理想
  • 服务端内部大量 RPC 调用时,序列化效率太重要了

于是有了 Protobuf:数据格式紧凑、序列化速度快、跨语言支持也强。

它不是为了可读性,而是为了性能。


PHP 解析 Protobuf 为什么麻烦

PHP 能解析 Protobuf,但体验不如其他语言。原因有几个,简单列一下:

PHP 无法动态解析 Schema

像 Go、Python、Java 这类语言可以依靠 descriptor 动态解析 Protobuf 数据结构,甚至可以在运行期处理未知结构。

PHP 目前做不到,没有暴露那一套 API。

所以 PHP ​必须依赖 .proto 文件,并且必须提前用 protoc 生成对应的 PHP 类。

PHP 的 Protobuf 扩展是“最小实现”

google/protobuf 的 PHP 扩展只提供:

  • 序列化:serialize
  • 反序列化:mergeFrom
  • 基本的 getter/setter 机制

其他高级能力基本没有。

PHP 的生态也不会把“解析二进制协议”当作主要用途

PHP 的常见使用场景偏 Web,因此处理二进制协议并不是重点。

并不是我们主动选择 Protobuf

在一些服务里,上游服务已经定死使用 Protobuf;或者 PHP 服务只是边缘网关,需要解析一次再转发。

在这种情况下,只有硬着头皮支持。

如果是自己的项目,并没有强约束,其实 JSON 足够了。


PHP 如何使用 Protobuf

我自己在服务器上没有安装 Protobuf 扩展,而是采用更常见的一种方式:

  • 本地安装 protoc
  • .proto 文件生成 PHP 类
  • 服务器端只需要安装 google/protobuf 包即可完成解析

第一步:安装运行时库

composer require google/protobuf

这是 PHP 解析 Protobuf 所需的唯一运行时依赖。

第二步:安装 protoc(在本地)

protoc 是官方编译器,用于把 .proto 文件生成各种语言的类(包括 PHP)。

下载地址:

github.com/protocolbuf…

选择对应平台的压缩包,解压后把 protoc 放到 PATH 中即可。

验证是否安装成功:

protoc --version

.proto 文件是什么

.proto 文件可以简单理解为“数据结构的一份字典”。

因为 Protobuf 的二进制格式里没有字段名,只有字段编号(tag)。

例如:

field #1:  123
field #2: "Alice"

你不知道 #1 是 id​ 还是 age​,也不知道 #2 是 name 还是别的东西。

所以必须依靠 .proto 文件才能解码。


使用 protoc 生成 PHP 类

我本地的命令大致如下:

protoc --php_out=./protobuf 
       --proto_path=./protobuf 
       xxx.proto

含义如下:

  • --php_out:生成的 PHP 文件存放位置
  • --proto_path​:寻找 .proto 的目录
  • 多个 .proto 可以一起编译

protoc 会根据 .proto​ 内容生成一堆 PHP 类,每个 message 对应一个 PHP 类,最终这些类会继承:

GoogleProtobufInternalMessage

序列化、反序列化功能都来自这个基类。


配置 Composer autoload

如果你希望通过命名空间加载生成的类,可以在 composer.json 中加一条:

"autoload": {
    "psr-4": {
        "Proto\": "protobuf/"
    }
}

然后执行:

composer dumpautoload

在 PHP 中解析 Protobuf

解析的核心方法是:

$msg->mergeFromString($binary)

读完后,数据结构会自动填充在 message 对象里。


在 PHP 中生成 Protobuf 数据

序列化对应的方法是:

$binary = $msg->serializeToString();

得到的就是一段 protobuf 二进制字符串,可以直接发送到网络或写入文件。


快速测试

创建一个项目,目录结构如下:

.
├── protobuf
│   └── TEST_USER_INFO.proto
└── test.php

TEST_USER_INFO.proto

syntax = "proto3";

option php_namespace = "TestUserInfo";

message User {
  int32 id = 1;
  string name = 2;
}

test.php

<?php

require __DIR__ . '/vendor/autoload.php';

use TestUserInfoUser;

$u1 = new User();
$u1->setId(7);
$u1->setName("PHP Encode Test");

$bin = $u1->serializeToString();

$u2 = new User();
$u2->mergeFromString($bin);

var_dump([
    '原始数据' => bin2hex($bin),
    'id' => $u2->getId(),
    'name' => $u2->getName(),
]);

安装运行时库

composer require google/protobuf

编译 .proto 文件

protoc --php_out=./protobuf --proto_path=./protobuf TEST_USER_INFO.proto

这会在 protobuf/ 目录下生成 PHP 类文件,供 PHP 使用。


配置 Composer autoload

composer.json 中增加命名空间映射,例如:

{
  "require": {
    "google/protobuf": "^4.33"
  },
  "autoload": {
    "psr-4": {
      "GPBMetadata\": "protobuf/GPBMetadata",
      "TestUserInfo\": "protobuf/TestUserInfo"
    }
  }
}

然后重新加载 Composer 自动加载:

composer clear-cache && composer dump-autoload -o

运行测试

php test.php

运行后,你会看到序列化再反序列化的数据被正确输出,证明 PHP 成功处理了 Protobuf 数据。

写在最后

PHP 解析 Protobuf 的体验确实不算好,但能用,并且在某些需要兼容上游服务的场景里还是必须用。
如果你也正在处理类似的数据,希望这篇文章能帮你少踩点坑。

如果感觉文章里哪部分还没说清楚,欢迎继续交流。

本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]