Protobuf 小試牛刀

本文以PHP爲例。

環境: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/prot...html

是什麼

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

官方文檔:https://github.com/protocolbu...python

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

咱們可使用Protobuf:git

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

安裝

安裝清單一覽:github

  • protoc
  • 各編程語言對應的protobuf庫

安裝protoc

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

地址:https://github.com/protocolbu...數據庫

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/protocolbu...編程

windows版地址:
https://github.com/protocolbu...

而後命令行輸入 protoc能夠查看幫助。

假設有一個 .proto格式的文件,須要編譯成其它語言代碼,以PHP爲例則是:

mkdir php
protoc --php_out=php  *.proto

其中--php_out=php表示編譯成PHP代碼,放在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.

後面有示例說明。

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

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

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

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

PHP擴展安裝

php能夠安裝c擴展版本或者純php代碼版本。

C擴展版本

一、下載擴展源碼:

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

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

extension=protobuf.so

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

純PHP版本

使用 composer 安裝便可:

composer require google/protobuf

下面說一下區別和注意事項:
一、截止到3.8.0版本,若是安裝的是純PHP版本,protobuf 裏提供的序列化方法serializeToJsonString()不支持參數,c擴展版本支持,表示保留proto裏定義的屬性,不進行轉大寫;
二、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...

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

須要在proto頭部申明:

syntax = "proto3";

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

二、註釋
使用 //,示例:

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

其中寫在每一個屬性後面的註釋在生產的代碼裏面有保留。

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

四、字段規則
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。

五、支持的數據類型

詳情參看官方文檔:https://developers.google.com...

六、默認值說明

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

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

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

八、引用類型
上面的UserList就引用了User類型。你們能夠看一下。

九、import
若是一個proto文件引用了另一個proto文件,那麼可使用import關鍵字在頭部申明:

import "User.proto";

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

map<key_type,value_type> map_field = N;

示例:

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

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

十、Any
Any類型容許包裝任意的message類型,能夠經過pack()unpack()(方法名在不一樣的語言中可能不一樣)方法打包/解包:

import "google/protobuf/any.proto";

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

PHP開發的同窗可能以爲Any不必,由於數組裏任何類型均可以放,可是對於強類型語言,數組裏的值類型必須是一致的,使用Any類型能夠解決這個問題。Any至關於把值包裝了一層,這樣都是Any類型。

十一、服務定義

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

這至關於定義了一個類,裏面有一個對外的GetUser()方法。這個一般用於定義RPC服務,與gRPC結合使用。

十二、從.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插件

一、JetBrains PhpStorm 能夠在插件裏找到Protobuf安裝,重啓IDE後就支持proto格式語法了。

二、VScode 在擴展裏搜索 Protobuf,安裝便可。

三、protobuf的 php 擴展類在ide中沒有提示,可將https://github.com/protocolbu... 目錄下載到本地,將此目錄加到ide的include_path中便可。

常見問題

一、 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/

二、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

參考

一、protoc2 與 protoc3 區別 - 簡書
https://www.jianshu.com/p/cde...
二、gRPC之proto語法 - 簡書
https://www.jianshu.com/p/da7...
三、Protobuf3語法詳解 - 望星辰大海 - 博客園
https://www.cnblogs.com/tohxy...

相關文章
相關標籤/搜索