再来一次手牵手:php7.0 + protobuf

这两天在折腾 php。

php 作为世界世界上最好的编程语言,我不过是读研那会给导师做项目的时候撸过一年的代码,水平一般,只了解基本的语法,会进行简单的 php 后台开发,后来就没再用过了。

这次,是为了工作需要,在整理项目模块的时候,同事反映“之前的 php5.x 对 protobuf 的支持不是很好,解码出来的数据结构跟其它语言的对不上……”, 我当时的第一反应是:居然有这种事?至于具体是不是真的如此,我也懒得去重现。那是两年前的事,现在 php 都到 7.x ,protobuf 都到 3.5.1 了,时过境迁,IT 技术日新月异,或许问题早就解决了呢?

报着试一试的态度,我主动提出来调研下 “protobuf 在 php7.0 的用法”。

php

docker 里面现成的 php 镜像多的是,想要快的话,直接 run 一个 php 容器出来就可以,但是我想从零开始,体验下 Linux 配置 php 开发环境的流程,一步一个脚印,给人一种稳重的安全感。

所以,所以还是从一个裸体的 ubuntu 系统开始吧。

启动一个 ubuntu:16.04 容器:

1
docker run -d -P -it ubuntu:16.04 bash

先装 php 的,这里我选了 7.0 版本:

1
2
3
4
5
6
7
root@aae76b5e0ca4:/var/www/html# apt-get update
root@aae76b5e0ca4:/var/www/html# apt-get install php7.0
root@aae76b5e0ca4:/var/www/html# php -v
PHP 7.0.22-0ubuntu0.16.04.1 (cli) ( NTS )
Copyright (c) 1997-2017 The PHP Group
Zend Engine v3.0.0, Copyright (c) 1998-2017 Zend Technologies
with Zend OPcache v7.0.22-0ubuntu0.16.04.1, Copyright (c) 1999-2017, by Zend Technologies

装个 nginx 服务器来驱动 php 脚本吧:

1
2
3
root@aae76b5e0ca4:/var/www/html# apt-get intall nginx
root@aae76b5e0ca4:/var/www/html# nginx -v
nginx version: nginx/1.10.3 (Ubuntu)

配置下 nginx,开启 php 解释器, 修改 /etc/nginx/sites-enabled/default 这部分如下:

1
2
3
4
5
6
7
location ~ \.php$ {
include snippets/fastcgi-php.conf;
# With php7.0-cgi alone:
# fastcgi_pass 127.0.0.1:9000;
# With php7.0-fpm:
fastcgi_pass unix:/run/php/php7.0-fpm.sock;
}

启动 PHP 和 nginx 服务:

1
2
3
root@aae76b5e0ca4:/var/www/html# service php7.0-fpm start
root@aae76b5e0ca4:/var/www/html# service nginx start
* Starting nginx nginx [ OK ]

这样,php 运行环境就搭建好了,可以写个简单的 PHP 脚本 helloworld.php 来验证下,把它放在 nginx 默认的网站根目录 /var/www/html 中:

1
2
3
<?php
echo "Hello World!";
?>

现在就是见证奇迹的时刻:

1
2
root@aae76b5e0ca4:/var/www/html# curl 127.0.0.1/helloworld.php
Hello World!

只需几步就配置好了 php 运行环境,真是 Excited!

protobuf

php7 要使用 Google protobuf 需要安装两个东西:protoc 编译器php 扩展库

protoc 编译器

https://github.com/google/protobuf/releases 下载 linux 平台的 protoc ,把 bininclude 里面的文件拷贝到 /usr/local 的对应目录,这样 protoc 就算安装完成了:

1
2
root@aae76b5e0ca4:/var/www/html# protoc --version
libprotoc 3.5.1

写个 test.proto

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
syntax = "proto3";

message PhoneNumber {
string number = 1;
int32 type = 2;
}

message Person {
string name = 1;
int32 id = 2;
string email = 3;
repeated PhoneNumber phone = 4;
double money = 5;
}

message AddressBook {
repeated Person person = 1;
}

这个 proto 文件是什么意思,相信用过 proto 的人都知道。

编译 proto 文件:

1
2
3
4
5
6
7
8
9
root@aae76b5e0ca4:/var/www/html# mkdir foo
root@aae76b5e0ca4:/var/www/html# protoc --php_out=foo test.proto
root@aae76b5e0ca4:/var/www/html# tree foo/
foo/
|-- AddressBook.php
|-- GPBMetadata
| `-- Test.php
|-- Person.php
`-- PhoneNumber.php

可以看到,protoc 编译出的结果里包含三个 php 文件(对应 proto 文件里面声明的三种结构)和 一个 GPBMetadata 文件夹(下面的 Test.php 描述了 test.proto 文件的元信息)。

php 扩展库

protobuf 的 php 扩展 官方 提供了 c 动态库 和 php 包两种形式。

php 包形式我折腾了半天也不会用,烦死了。还是安装 c 动态库形式的扩展吧:

1
2
3
4
5
6
7
8
root@aae76b5e0ca4:/var/www/html# apt-get install php-pear php5-dev autoconf automake libtool make gcc
root@aae76b5e0ca4:/var/www/html# pecl install protobuf-3.5.1
...
Build process completed successfully
Installing '/usr/lib/php/20151012/protobuf.so'
install ok: channel://pecl.php.net/protobuf-3.5.1
configuration option "php_ini" is not set to php.ini location
You should add "extension=protobuf.so" to php.ini

提示安装成功,但还需要修改 php 的配置文件 /etc/php/7.0/fpm/php.ini, 开启 protobuf 库支持:

1
2
;extension=php_xsl.dll
extension=protobuf.so

重启 php7.0-fpm 服务。

Test

准备工作做好后,就可以正式进行 protobuf 解析了。

写个测试例子 test.php

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
require_once "foo/GPBMetadata/Test.php";
require_once "foo/AddressBook.php";
require_once "foo/Person.php";
require_once "foo/PhoneNumber.php";

$foo = new Person();

$foo->setName('hxz');
$foo->setId(2);
$foo->setEmail('notexist@foxmail.com');
$foo->setMoney(1988894.995);

$phone_num = new PhoneNumber();
$phone_num->setNumber('1351010xxxx');
$phone_num->setType(3);

$phone = array();
array_push($phone, $phone_num);
$foo->setPhone($phone);

$packed = $foo->serializeToString();

try {
$p = new Person();
$p->mergeFromString($packed);
echo "------------parsed-------\n";
echo $p->getName() ."\n";
echo $p->getEmail() ."\n";
echo $p->getMoney() ."\n";
echo $p->getId() . "\n";
echo $p->getPhone()[0]->getNumber() ."\n";
} catch (Exception $ex) {
die('Upss.. there is a bug in this example');
}
?>

访问 127.0.0.1/test.php 输出:

1
2
3
4
5
6
7
root@aae76b5e0ca4:/var/www/html# curl localhost/test.php
------------parsed-------
hxz
notexist@foxmail.com
1988894.995
2
1351010xxxx

看上去,protobuf 的 serializeToStringmergeFromString 都没有问题。

一切看上去都那么自然,但,这里有个问题,不得不提下。测试发现:

test.php 中,必须引入 proto 文件中声明过的所有结构类型(即使你只用到了里面的一个类型),否则会报错。

比如,注释掉这行 require_once "foo/AddressBook.php";(事实上,AddressBook 这个类也没有用到), nginx-error 日志输出:

1
2018/01/19 08:32:10 [error] 6880#6880: *39 recv() failed (104: Connection reset by peer) while reading response header from upstream, client: 127.0.0.1, server: _, request: "GET /test.php HTTP/1.1", upstream: "fastcgi://unix:/run/php/php7.0-fpm.sock:", host: "localhost"

这个就很恶心了,“我只想要个苹果,你却给我一车梨?”。具体原因我还不知道,谁能帮我解释下吗?

总结

php 本身的确是门很简单易用的语言,加上我当年学到的东西还没有忘光,所以重新拾起来还是有故人重逢的感觉。

小试牛刀发现:php7.0 下 protobuf 扩展是可以用的,配置起来也不是很难。

然后,我在调研过程中,还是留了 2 个坑:

  • php 扩展包的另外一种安装方式
  • 拔一发而动全身:必须引入 proto 文件中声明过的所有结构类型

有收获,也有遗憾,这次 “php7.0+protobuf” 的填坑之旅就这样告一段落吧,具体的业务代码,就让 phper 们去撸吧, 我还是喜欢用 go。