• 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相关的命名空间问题,这里我们定义的命名空间是SampleModel,但是 GPBMetadata/User.php以及Sample/Model/User.php的命名空间我们希望调整下,都以SampleModel开头,而不是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 SampleModelUser;
    use SampleModelUserList;
    
    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 
    SampleModelUserList Object
    /work/git/protobuf-sample/tests/user.php:15:
    int(1)
    GoogleProtobufInternalRepeatedField Object
    (
    )
    SampleModelUser Object
    1
    {"list":[{"id":1,"name":"test"}],"page":1,"limit":5}
    

    可以看到使用var_dump、print_r等函数是打印不出来 protobuf生成的对象的,但是里面确实是有内容的,只有标量能打印出来,或者序列化为字符串。

    我们也可以将一个字符串反序列化为protobuf对象:
    user_merge.php

    <?php 
    use SampleModelUserList;
    
    $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
    
    SampleModelUserList 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

  • 相关阅读:
    推荐一款快得令地发指本地搜索软件:Everything,绝对改变对NTFS的看法
    “/”应用程序中的服务器错误 WebParts开发时出现的错误
    《让人无法说 NO的攻心说话术》摘要
    UXWEEK
    2012中国交互设计体验日演讲实录
    彩色铅笔入门
    ClickOnce证书签名
    DevExpress控件使用小结
    解决ClickOnce签名过期问题
    属于自己的小小空间
  • 原文地址:https://www.cnblogs.com/52fhy/p/11106670.html
Copyright © 2020-2023  润新知