Protobuf-小试牛刀

40次阅读

共计 7564 个字符,预计需要花费 19 分钟才能阅读完成。

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

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

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

测试编译生成的代码

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

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/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…

正文完
 0