前陣子做的一個直播彈幕的機器人,其中有一部分上游數據是通過 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 的體驗確實不算好,但能用,並且在某些需要兼容上游服務的場景裏還是必須用。
如果你也正在處理類似的數據,希望這篇文章能幫你少踩點坑。
如果感覺文章裏哪部分還沒説清楚,歡迎繼續交流。