39

Protobuf 小试牛刀

 4 years ago
source link: https://www.tuicool.com/articles/BVz6F3u
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

本文以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...

是什么

Protobuf是一种平台无关、语言无关、可扩展且轻便高效的序列化数据结构的协议,可以用于网络通信和数据存储。

官方文档: https://github.com/protocolbu...

作为数据交换协议,常见的还有JSON、XML。相比JSON,Protobuf有更高的转化效率。一般JSON用于HTTP接口,Protobuf用于RPC比较多。以gRPC为例,默认就是使用Protobuf。

我们可以使用Protobuf:

  • 作为RPC的序列化数据结构的协议。类似于JSON
  • 定义proto文件,一键生成多语言代码。

安装

安装清单一览:

  • protoc
  • 各编程语言对应的protobuf库

安装protoc

为了将proto文件转成编程语言代码,需要安装编译工具。

地址: 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 。

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目标代码,请执行以下步骤:

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

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 代码部分示例:

1460000019688460?w=1156&h=528

测试编译生成的代码

接下来,我们写个例子看看如何使用生成的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...

1、proto3proto 有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、支持的数据类型 1460000019688461

详情参看官方文档: https://developers.google.com...

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、AnyAny类型允许包装任意的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/protocolbu... 目录下载到本地,将此目录加到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/cde...

2、gRPC之proto语法 - 简书

https://www.jianshu.com/p/da7...

3、Protobuf3语法详解 - 望星辰大海 - 博客园

https://www.cnblogs.com/tohxy...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK