本文以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; //分頁條數
}
以上分別創建了user
和UserList
兩個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里是結構體。每一個屬性都會生成對應的getXXX
、setXXX
方法。
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