博客 / 詳情

返回

用 PHP 解析 Protobuf 的坑與解法

前陣子做的一個直播彈幕的機器人,其中有一部分上游數據是通過 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)。

下載地址:

https://github.com/protocolbuffers/protobuf/releases

選擇對應平台的壓縮包,解壓後把 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 類,最終這些類會繼承:

Google\Protobuf\Internal\Message

序列化、反序列化功能都來自這個基類。


配置 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 TestUserInfo\User;

$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 的體驗確實不算好,但能用,並且在某些需要兼容上游服務的場景裏還是必須用。
如果你也正在處理類似的數據,希望這篇文章能幫你少踩點坑。

如果感覺文章裏哪部分還沒説清楚,歡迎繼續交流。

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.