Protobuf 小試牛刀


本文以PHP為例。

環境:

  • CentOS 6.8
  • proto 3.8
  • PHP 7.1.12
  • PHP protobuf擴展 3.8.0
  • go1.12.5 linux/amd64

本文示例倉庫地址: https://github.com/52fhy/protobuf-sample

是什么

Protobuf是一種平台無關、語言無關、可擴展且輕便高效的序列化數據結構的協議,可以用於網絡通信和數據存儲。

官方文檔:https://github.com/protocolbuffers/protobuf

作為數據交換協議,常見的還有JSON、XML。相比JSON,Protobuf有更高的轉化效率。一般JSON用於HTTP接口,Protobuf用於RPC比較多。以gRPC為例,默認就是使用Protobuf。

我們可以使用Protobuf:

  • 作為RPC的序列化數據結構的協議。類似於JSON
  • 定義proto文件,一鍵生成多語言代碼。

安裝

安裝清單一覽:

  • protoc
  • protoc-gen-go 編譯出golang目標代碼
  • protoc-gen-doc 文檔生成工具支持
  • 各編程語言對應的protobuf庫

安裝protoc

為了將proto文件轉成編程語言代碼,需要安裝編譯工具。

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

wget https://github.com/protocolbuffers/protobuf/releases/download/v3.8.0/protoc-3.8.0-linux-x86_64.zip
unzip protoc-3.8.0-linux-x86_64.zip
cp bin/protoc /usr/bin/
cp -r include/google /usr/include/

注:最后一行是為了將proto的一些庫復制到系統,例如google/protobuf/any.proto,如果不復制,編譯如果用了里面的庫例如Any,會提示:protobuf google.protobuf.Any not found 。

mac版地址:
https://github.com/protocolbuffers/protobuf/releases/download/v3.8.0/protoc-3.8.0-osx-x86_64.zip

windows版地址:
https://github.com/protocolbuffers/protobuf/releases/download/v3.8.0/protoc-3.8.0-win64.zip

然后命令行輸入 protoc可以查看幫助。

假設有一個 .proto格式的文件,需要編譯成其它語言代碼:

mkdir -p sdk/php
protoc --php_out=sdk/php  --java_out=sdk/java --js_out=sdk/js --objc_out=sdk/objc  *.proto

其中--php_out=sdk/php表示編譯成PHP代碼,放在sdk/php目錄。protof支持的語言:

$ protoc | grep "=OUT_DIR"
  --cpp_out=OUT_DIR           Generate C++ header and source.
  --csharp_out=OUT_DIR        Generate C# source file.
  --java_out=OUT_DIR          Generate Java source file.
  --js_out=OUT_DIR            Generate JavaScript source.
  --objc_out=OUT_DIR          Generate Objective C header and source.
  --php_out=OUT_DIR           Generate PHP source file.
  --python_out=OUT_DIR        Generate Python source file.
  --ruby_out=OUT_DIR          Generate Ruby source file.

默認沒有go代碼支持,如果需要支持go的代碼生成,則需要protoc-gen-go工具。

golang 代碼編譯支持
protoc --help 並沒有--go_out參數說明, 如需編譯golang目標代碼,請執行以下步驟:

1、安裝golang環境:yum install golang,其它系統查看 https://studygolang.com/dl (已安裝請跳過)
2、go get github.com/golang/protobuf/protoc-gen-go
3、復制擴展工具到/usr/bin:

cp `go env|grep 'GOPATH'|sed -e 's/GOPATH="//' -e 's/"//'`/bin/protoc-gen-go /usr/bin/

4、編譯go目標代碼: protoc --go_out=./go *.proto

protoc-gen-doc文檔生成工具支持

1、需要golang環境
2、go get -u github.com/pseudomuto/protoc-gen-doc/cmd/protoc-gen-doc
3、復制擴展工具到/usr/bin:

cp `go env|grep 'GOPATH'|sed -e 's/GOPATH="//' -e 's/"//'`/bin/protoc-gen-doc /usr/bin/

4、編譯proto生成HTML文檔: --plugin=/usr/bin/protoc-gen-doc --doc_out=html,index.html:./doc

一個完整的例子:

p=$(cd `dirname $0`;pwd)
namespace="Pb_$appname"

cd $p/proto/

rm -rf $p/sdk/php
mkdir $p/sdk/php

protoc 
  --plugin=/usr/bin/protoc-gen-doc --doc_out=html,index.html:$p/doc  \
  --php_out=$p/sdk/php  \
  --grpc_out=$p/sdk/php --plugin=protoc-gen-grpc=/usr/local/bin/grpc_php_plugin  \
  --go_out=plugins=grpc:$p/$namespace/   \
  --java_out=$p/sdk/java  \
  --js_out=$p/sdk/js  \
  --objc_out=$p/sdk/objc \
  *.proto

--grpc_out=$p/sdk/php --plugin=protoc-gen-grpc=/usr/local/bin/grpc_php_plugin這個用於生成grpc代碼,如果沒有可以去掉。

PHP擴展安裝

php可以安裝c擴展版本或者純php代碼版本。

C擴展版本

1、下載擴展源碼:

wget https://pecl.php.net/get/protobuf-3.8.0.tgz
tar zxf protobuf-3.8.0.tgz
cd protobuf-3.8.0
phpize
./configure
make
sudo make install

或者直接使用 pecl 安裝:

pecl install protobuf-3.8.0

2、 輸入 php -i|grep php.ini 查看php.ini的路,修改php.ini, 增加:

extension=protobuf.so

3、檢查是否安裝成功:php --ri protobuf,安裝成功會顯示版本號。

純PHP版本

使用 composer 安裝即可:

composer require google/protobuf

下面說一下區別和注意事項:
1、截止到3.8.0版本,如果安裝的是純PHP版本,protobuf 里提供的序列化方法serializeToJsonString()不支持參數,c擴展版本支持,表示保留proto里定義的屬性,不進行轉大寫;
2、c擴展版本無法使用var_dump等函數打印出protobuf對象里的對象的結構和內容,但是如果protobuf對象里的標量類型是可以打印出來的。

Go擴展庫安裝

golang如果使用protobuf,需要引入google.golang.org/grpc庫。使用 go mod管理,可以編寫規則做個映射:

replace google.golang.org/grpc => github.com/grpc/grpc-go v1.21.1

應用:protobuf創建Model

有時候我們需要根據數據庫表結構生成一個Model,常規辦法是手寫,比較麻煩。有了protobuf,我們可以先編寫一個proto 文件,然后編譯成目標語言的代碼。

定義proto

我們先定義一個 proto 文件:

// proto/User.proto
syntax = "proto3";
package Sample.Model; //namesapce

message User {
    int64 id = 1; //主鍵id
    string name = 2; //用戶名
    string avatar = 3; //頭像
    string address = 4; //地址
    string mobile = 5; //手機號
    map<string, string> ext = 6; //擴展信息
}

message UserList {
    repeated User list = 1; //用戶列表
    int32 page = 2; //分頁
    int32 limit = 3; //分頁條數
}

以上分別創建了userUserList兩個Model。

編譯proto

現在使用proto工具編譯出來:

mkdir php
protoc --php_out=php proto/User.proto

會生成:

├── php
│   ├── GPBMetadata
│   │   └── User.php
│   └── Sample
│       └── Model
│           ├── UserList.php
│           └── User.php
├── proto
│   └── User.proto

UserList.php 代碼部分示例:

測試編譯生成的代碼

接下來,我們寫個例子看看如何使用生成的Model。在使用之前需要處理下GPBMetadata相關的命名空間問題,這里我們定義的命名空間是Sample\Model,但是 GPBMetadata/User.php以及Sample/Model/User.php的命名空間我們希望調整下,都以Sample\Model開頭,而不是GPBMetadata。下面我們使用命令行處理:

cd protobuf-sample

#修改GPBMetadata命名空間
cd php
mv -f GPBMetadata Sample/Model/

find . -name '*.php' ! -name example.php -exec sed -i -e 's#GPBMetadata#Sample\\Model\\GPBMetadata#g' -e 's#\\Sample\\Model\\GPBMetadata\\Google#\\GPBMetadata\\Google#g' {} \;

cd -

接下來我們寫個測試文件:
user.php

<?php

use Sample\Model\User;
use Sample\Model\UserList;

ini_set("display_errors", true);
error_reporting(E_ALL);
require_once "autoload.php";
$user = new User();
$user->setId(1)->setName("test");
$userList = new UserList();
$userList->setPage(1)->setLimit(5)->setList([$user]);

print_r($userList);
var_dump($userList->getPage());
print_r($userList->getList());

foreach ($userList->getList() as $key => $obj) {
    print_r($obj);
    echo $obj->getId() .PHP_EOL;
}

autoload.php是實現自動加載的。

我們運行:

$ php tests/user.php 
Sample\Model\UserList Object
/work/git/protobuf-sample/tests/user.php:15:
int(1)
Google\Protobuf\Internal\RepeatedField Object
(
)
Sample\Model\User Object
1
{"list":[{"id":1,"name":"test"}],"page":1,"limit":5}

可以看到使用var_dump、print_r等函數是打印不出來 protobuf生成的對象的,但是里面確實是有內容的,只有標量能打印出來,或者序列化為字符串。

我們也可以將一個字符串反序列化為protobuf對象:
user_merge.php

<?php 
use Sample\Model\UserList;

$json = '{"list":[{"id":1,"name":"test"}],"page":1,"limit":5}';

require_once "autoload.php";

$userList = new UserList();
$userList->mergeFromJsonString($json);
print_r($userList);
echo $userList->serializeToJsonString();

運行示例:

$ php tests/user_merge.php

Sample\Model\UserList Object
{"list":[{"id":1,"name":"test"}],"page":1,"limit":5}

proto語法

這里只將介紹簡單的,如果需要細研究,請查看官方文檔。

官方文檔:https://developers.google.com/protocol-buffers/docs/overview

1、proto3
proto 有proto3 和 proto2。proto3 比 proto2 支持更多語言但 更簡潔。去掉了一些復雜的語法和特性,更強調約定而弱化語法。如果是首次使用 Protobuf ,建議使用 proto3 。詳見參考文獻說明。

需要在proto頭部申明:

syntax = "proto3";

如果你沒有指定這個,編譯器會使用proto2。

2、注釋
使用 //,示例:

message UserList {
    repeated User list = 1; //用戶列表
    int32 page = 2; //分頁
    int32 limit = 3; //分頁條數
}

其中寫在每個屬性后面的注釋在生產的代碼里面有保留。

3、message
message類似於結構體的概念,最終編譯為代碼在PHP、JAVA里就是一個類,在golang里是結構體。每一個屬性都會生成對應的getXXXsetXXX方法。

4、字段規則
repeated表示這個屬性重復N次,在相對應的編程語言中通常是一個空的list。PHP里對應數組。

reserved表示標識號保留暫時不用。

message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

在消息定義中,每個字段都有唯一的一個數字標識符。這些標識符是用來在消息的二進制格式中識別各個字段的,一旦開始使用就不能夠再改變。注:[1,15]之內的標識號在編碼的時候會占用一個字節。[16,2047]之內的標識號則占用2個字節。所以應該為那些頻繁出現的消息元素保留 [1,15]之內的標識號。切記:要為將來有可能添加的、頻繁出現的標識號預留一些標識號。最小的標識號可以從1開始,最大到2^29 - 1, or 536,870,911。

5、支持的數據類型

詳情參看官方文檔:https://developers.google.com/protocol-buffers/docs/proto3#scalar

6、默認值說明

  • string類型,默認值是空字符串
  • bytes類型,默認值是空bytes
  • bool類型,默認值是false
  • 數字類型,默認值是0
  • 枚舉類型,默認值是第一個枚舉值,即0
  • repeated修飾的屬性,默認值是空.

7、枚舉
使用enum關鍵字定義枚舉,值必須從0開始:

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

8、引用類型
上面的UserList就引用了User類型。大家可以看一下。

9、import
如果一個proto文件引用了另外一個proto文件,那么可以使用import關鍵字在頭部申明:

import "User.proto";

10、Map類型
proto支持map屬性類型的定義,語法如下:

map<key_type,value_type> map_field = N;

示例:

map<string, string> ext = 6; //擴展信息

這個map對於PHP來說就是關聯數組,對於golang來說就是Map。

10、Any
Any類型允許包裝任意的message類型,可以通過pack()unpack()(方法名在不同的語言中可能不同)方法打包/解包:

import "google/protobuf/any.proto";

message Response {
    google.protobuf.Any data = 1;
}

PHP開發的同學可能覺得Any沒必要,因為數組里任何類型都可以放,但是對於強類型語言,數組里的值類型必須是一致的,使用Any類型可以解決這個問題。Any相當於把值包裝了一層,這樣都是Any類型。

11、服務定義

service UserService {
    //  方法名  方法參數                 返回值
    rpc GetUser(Request) returns (Response); 
}

這相當於定義了一個類,里面有一個對外的GetUser()方法。這個通常用於定義RPC服務,與gRPC結合使用。

12、從.proto文件生成了什么?
當用protocol buffer編譯器來運行.proto文件時,編譯器將生成所選擇語言的代碼,這些代碼可以操作在.proto文件中定義的消息類型,包括獲取、設置字段值,將消息序列化到一個輸出流中,以及從一個輸入流中解析消息。

  • PHP:每一個Message或者Enum生成一個類,另外還會生成GPBMetadata
  • C++:編譯器會為每個.proto文件生成一個.h文件和一個.cc文件,.proto文件中的每一個消息有一個對應的類。
  • Java:編譯器為每一個消息類型生成了一個.java文件,以及一個特殊的Builder類(該類是用來創建消息類接口的)。
  • Python:Python編譯器為.proto文件中的每個消息類型生成一個含有靜態描述符的模塊,該模塊與一個元類(metaclass)在運行時(runtime)被用來創建所需的Python數據訪問類。
  • go:編譯器會位每個消息類型生成了一個.pd.go文件。
  • Ruby:編譯器會為每個消息類型生成了一個.rb文件。
  • Objective-C:編譯器會為每個消息類型生成了一個pbobjc.h文件和pbobjcm文件,.proto文件中的每一個消息有一個對應的類。
  • C#:編譯器會為每個消息類型生成了一個.cs文件,.proto文件中的每一個消息有一個對應的類。

其它

IDE插件

1、JetBrains PhpStorm 可以在插件里找到Protobuf安裝,重啟IDE后就支持proto格式語法了。

2、VScode 在擴展里搜索 Protobuf,安裝即可。

3、protobuf的 php 擴展類在ide中沒有提示,可將https://github.com/protocolbuffers/protobuf/tree/master/php/src 目錄下載到本地,將此目錄加到ide的include_path中即可。

常見問題

1、 protoc 編譯輸出php文件時遇到一個錯誤:protobuf google.protobuf.Any not found。
原因:安裝proto的時候沒有把include/google復制到/usr/include/
解決:重新下載protoc-3.8.0-linux-x86_64.zip並將解壓后的include/google復制到/usr/include/

2、Mac下執行phpize報如下錯誤:

grep: /usr/include/php/main/php.h: No such file or directory
grep: /usr/include/php/Zend/zend_modules.h: No such file or directory
grep: /usr/include/php/Zend/zend_extensions.h: No such file or directory

解決方法:

xcode-select --instal

參考

1、protoc2 與 protoc3 區別 - 簡書
https://www.jianshu.com/p/cdedcf696e9e
2、gRPC之proto語法 - 簡書
https://www.jianshu.com/p/da7ed5914088
3、Protobuf3語法詳解 - 望星辰大海 - 博客園
https://www.cnblogs.com/tohxyblog/p/8974763.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM