用PHP做gRPC的服务端

gRPC的官方文档中虽然没有给出PHP做Server的例子,但实际上grpc的扩展是支持的。

参考扩展中提供的几个类以及扩展内部的一些代码,写了一个简单的示例

这个示例只是证明PHP可以直接写gRPC服务,不代表可以在生产环境中这么用。主要原因有这么几点:

  1. 不借助pcntl或swoole的话是个单进程的服务,并发会阻塞
  2. 没做性能测试以及是否有内存泄漏的测试
  3. 而且直接PHP做Server的争议也比较大

完整示例代码(https://github.com/ssfyn/php-grpc-server-example

Server端的主要逻辑:

$this->server = new \Grpc\Server([]);
$this->server->addHttp2Port('0.0.0.0:50051');
$this->server->start();

while($request = $this->server->requestCall()){
    $method = $request->method;
    $call = $request->call;
    if ($method=='/dev.fyn.HelloWorld/SayHello') {
        //接收请求
        $recv = $call->startBatch([
            \Grpc\OP_RECV_MESSAGE => true
        ]);
        //将请求的数据转换为对象
        $request = new SayHelloRequest();
        $request->mergeFromString($recv->message);
        //调用业务代码
        $impl = new HelloWorldImpl();
        $response = $impl->sayHello($request);
        //处理返回值,即使没有metadata也要设置OP_SEND_INITIAL_METADATA,不然会一直阻塞在这里
        $call->startBatch([
            \Grpc\OP_SEND_INITIAL_METADATA => [],
            \Grpc\OP_SEND_MESSAGE => [
                'message'=>$response->serializeToString()
            ],
            \Grpc\OP_SEND_STATUS_FROM_SERVER => [
                'code'=>\Grpc\STATUS_OK,
                'details'=>'OK'
            ],
        ]);
    } else {
        $call->startBatch([
            \Grpc\OP_SEND_INITIAL_METADATA => [],
            \Grpc\OP_SEND_STATUS_FROM_SERVER => ['code'=>\Grpc\STATUS_NOT_FOUND,'details'=>'Not found'],
        ]);
    }
}

Mac安装PHP+GRPC

安装xcode命令行工具

xcode-select --install

安装brew

/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

通过brew安装php5.6(或其它版本)

brew install php@5.6

通过brew安装php切换器

brew install brew-php-switcher

切换php版本

brew-php-switcher 5.6

通过pecl安装PHP的protobuf扩展(要编译,比较慢)

pecl install protobuf

通过pecl安装PHP的grpc扩展(要编译,比较慢)

pecl install grpc

PHP通过runkit覆盖一个方法

首先需要安装runkit扩展

这是原有类

namespace Fyn;

class Hello
{
    public function say(string $name): string {
        return sprintf('Hello %s', $name);
    }
}

通过runkit覆盖(必须先将类load进来才能覆盖)

if (class_exists(\Fyn\Hello:class)) {
    runkit_method_redefine(
        \Fyn\Hello:class,
        'say',
        '$msg',
        'return "Nihao {$msg}";'
    );
}

更多方法:http://php.net/manual/zh/book.runkit.php

不建议在生产环境中使用

Composer如何指定php的版本

这个问题主要出现在本地开发环境的php版本高于生产环境运行的php版本,或构建服务时构建环境的p hp版本高于运行环境的php版本。这时composer会引入一些版本过高的包。可以在composer.json中的config.platform中设置php版本。

{
    "require": {
    }
    "config": {
        "platform" :{
            "php": "5.6",
            "ext-grpc": "1.14",
            "ext-protobuf": "3.5"
        }
    }
}

同时也可以写一下用到的一些扩展,这样在install的时候就不回去检查是否真的装了这些扩展

mysql查询字段中的条件判断

简单的值判断用CASE value WHEN compare_value THEN result ELSE result END

复杂的用IF(condition, true_result, false_result)

SELECT
	s.`id` AS '活动ID',
	s.`begin_date` AS '活动开始时间',
	s.`end_date` AS '活动结束时间',
	IF (s.`begin_date` > now() ,'未开始', IF(s.`end_date` < now() , '已结束' , '进行中' )) AS '状态',
	CASE s.`status` WHEN 1 THEN '已上线' ELSE '未上线' END AS '是否可用',
	sr.`product_id` AS '产品ID',
	sr.`sku_id` AS '库存ID'
FROM `activity_range` sr
LEFT JOIN `activity` s ON s.`id` = sr.`activity_id`

PHP动态创建类的例子

1.实现一个stream_wrapper

class ClassStream {
    private $pos;
    private $stream;
    private $path;
    private static $_cache = [];
    public function stream_open($path, $mode, $options, &$opened_path) {
        $this->path = $path;
        $this->stream = self::$_cache[$path]?:'';
        $this->pos = 0;
        return true;
    }
    public function stream_read($count) {
        $ret = substr($this->stream, $this->pos, $count);
        $this->pos += strlen($ret);
        return $ret;
    }
    public function stream_write($data){
        $l=strlen($data);
        $this->stream =
            substr($this->stream, 0, $this->pos) .
            $data .
            substr($this->stream, $this->pos += $l);
        self::$_cache[$this->path] = $this->stream;
        return $l;
    }
    public function stream_eof() {
        return $this->pos >= strlen($this->stream);
    }
    public function stream_stat(){
        return true;
    }
    public function url_stat($path, $flags){
        if (!array_key_exists($path, self::$_cache)) {
            return false;
        }
        return [];
    }
}

stream_wrapper_register('class',ClassStream::class);

 

2.自动加载器

spl_autoload_register(function ($className){
    $path = 'class://' . $className;
    if(file_exists($path)) {
        require $path;
    }
});

 

3.动态创建一个类

$path = 'class://TestA';

$code = '
<?php
class TestA {
    public $b = 0;
}
';

file_put_contents($path, $code);

 

4.调用

$obj = new TestA();
var_dump($obj);

 

HTML Purifier设置允许base64图片

$config = HTMLPurifier_Config::createDefault();
$config->set('URI.AllowedSchemes', ['data'=>true,'http'=>true,'https'=>true]);
$purifier = new HTMLPurifier($config);
$clean_html = $purifier->purify($dirty_html);

不可以先get出原始数据,然后增加data=>true,再set回去。

这个地方有个奇葩的逻辑,一旦执行get就会被finalize,再set就直接抛异常,而且是fatal error。

php安装zookeeper扩展

1.安装zookeeper lib

下载zookeeper
wget http://mirror.bit.edu.cn/apache/zookeeper/zookeeper-3.4.11/zookeeper-3.4.11.tar.gz

解压缩
tar -xzf zookeeper-3.4.11.tar.gz

进入代码目录
cd zookeeper-3.4.11/src/c/

配置编译后的so的生成路径
./configure -prefix=/usr/local/zookeeper-lib/

编译
make && make install

2.安装php扩展

下载php-zookeeper
git clone https://github.com/andreiz/php-zookeeper.git
cd php-zookeeper
phpize

配置zookeeper-lib的路径和php的路径
./configure –with-libzookeeper-dir=/usr/local/zookeeper-lib/ –with-php-config=/usr/local/php/bin/php-config

编译
make && make install

3.改php配置文件

vim /usr/local/php/lib/php.ini

增加:

extension=zookeeper.so

ex.重启php
supervisorctl restart php-fpm