目录

环境条件

  • PHP 7.0 +

  • PECL

    sudo apt-get install php7.0 php7.0-dev php-pear phpunit
    

    sudo apt-get install php php-dev php-pear phpunit
    
  • Composer

    $ curl -sS https://getcomposer.org/installer | php
    $ sudo mv composer.phar /usr/local/bin/composer
    

    或者Ubuntu安装composer

  • PHPUnit (可选)

    $ wget https://phar.phpunit.de/phpunit-old.phar
    $ chmod +x phpunit-old.phar
    $ sudo mv phpunit-old.phar /usr/bin/phpunit
    

安装gRPC扩展

gRPC扩展有两种安装方式:pecl直接安装,也可以通过phpize进行源码编译安装。

  • 通过pecl安装gRPC扩展

    sudo pecl install grpc
    

    或指定版本

    sudo pecl install grpc-1.7.0
    

    吐槽:install等的不耐烦了,新闻一条一条的看完了,还在闪烁着满屏的字母…

    总算好了:

    Build process completed successfully
    Installing '/usr/lib/php/20160303/grpc.so'
    install ok: channel://pecl.php.net/grpc-1.36.0
    configuration option "php_ini" is not set to php.ini location
    You should add "extension=grpc.so" to php.ini
    
    
  • 通过源码安装gRPC扩展

    • centos只能通过源码安装

      git clone -b v1.35.0 https://github.com/grpc/grpc
      
    • 构建及安装gRPC核心库

      $ cd grpc
      $ git submodule update --init
      $ make
      $ sudo make install
      
    • 构建并安装gRPC PHP扩展

      编译gRPC PHP扩展

      $ cd grpc/src/php/ext/grpc
      $ phpize
      $ ./configure
      $ make
      $ sudo make install
      

      这会将gRPC PHP扩展编译并安装到标准PHP扩展目录中。

  • 更新php.ini

    • 更新php.ini

      extension=grpc.so
      
    • 将gRPC PHP库添加为Composer依赖项

      您需要将此添加到项目的composer.json文件中。

      "require": {
          "grpc/grpc": "v1.7.0"
      }
      

Protobuf运行时库

Protobuf有两种安装方式:pecl扩展方式和composer包方式。

  • C实现方式

    $ sudo pecl install protobuf
    

    或指定

    $ sudo pecl install protobuf-3.4.0
    

    扩展添加到php.ini

    extension=protobuf.so
    
  • php实现方式

    将此添加到您的composer.json文件:

    "require": {
        "google/protobuf": "^v3.3.0"
    }
    

PHP Protoc插件

gRPC PHP protoc插件来生成客户端stub classes。它可以根据.proto服务定义生成服务器和客户端代码。

插件可能会使用新的protobuf版本的新功能,因此也请确保安装的protobuf版本与您构建此插件的grpc版本兼容。

$ git clone -b v1.35.0 https://github.com/grpc/grpc
$ cd grpc
$ git submodule update --init
$ mkdir -p cmake/build
$ pushd cmake/build
$ cmake ../..
$ make protoc grpc_php_plugin
$ make install
$ popd

注意:看官方在quick start章节中Protobuf的安装文档是有误的,以basics tutorial章节的为准(github issue提出)。另外注意在make protoc grpc_php_plugin之后没有生成protoc执行文件的话,可以make install生成相关执行文件。

官方示例

.proto服务定义

.proto文件用于定义服务,并用于生成服务stub classes。

比如官方示例中:

cd grpc/examples/protos

protos/
├── auth_sample.proto
├── BUILD
├── hellostreamingworld.proto
├── helloworld.proto
├── keyvaluestore.proto
├── README.md
└── route_guide.proto

这里我们看下helloworld.proto的rpc定义:

syntax = "proto3";

option java_multiple_files = true;
option java_package = "io.grpc.examples.helloworld";
option java_outer_classname = "HelloWorldProto";
option objc_class_prefix = "HLW";

package helloworld;

// The greeting service definition.
service Greeter {
  // Sends a greeting
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

// The request message containing the user's name.
message HelloRequest {
  string name = 1;
}

// The response message containing the greetings
message HelloReply {
  string message = 1;
}

生成gRPC代码

生成的代码用于rpc客户端。

$ cd examples/php
$ ls -a
composer.json  echo  .gitignore  greeter_client.php  greeter_proto_gen.sh  README.md  route_guide  run_greeter_client.sh
./greeter_proto_gen.sh

看下生成代码的greeter_proto_gen.sh文件内容:

#protoc --proto_path=../protos --php_out=. --grpc_out=. --plugin=protoc-gen-grpc=../../bins/opt/grpc_php_plugin ../protos/helloworld.proto

protoc --proto_path=../protos --php_out=. --grpc_out=. --plugin=protoc-gen-grpc=../../cmake/build/grpc_php_plugin ../protos/helloworld.proto

注意:shell文件中指定了../protos/helloworld.proto服务文件,根据该服务文件生成相应的stub classess。

在当前目录下生成 HelloWorld 目录:

$ tree ./Helloworld/
./Helloworld/
├── GreeterClient.php
├── HelloReply.php
└── HelloRequest.php

GreeterClient.php

namespace Helloworld;

/**
 * The greeting service definition.
 */
class GreeterClient extends \Grpc\BaseStub {

    /**
     * @param string $hostname hostname
     * @param array $opts channel options
     * @param \Grpc\Channel $channel (optional) re-use channel object
     */
    public function __construct($hostname, $opts, $channel = null) {
        parent::__construct($hostname, $opts, $channel);
    }

    /**
     * Sends a greeting
     * @param \Helloworld\HelloRequest $argument input argument
     * @param array $metadata metadata
     * @param array $options call options
     * @return \Grpc\UnaryCall
     */
    public function SayHello(\Helloworld\HelloRequest $argument,
      $metadata = [], $options = []) {
        return $this->_simpleRequest('/helloworld.Greeter/SayHello',
        $argument,
        ['\Helloworld\HelloReply', 'decode'],
        $metadata, $options);
    }

}

composer安装grpc与protobuf包:

$ composer install
Loading composer repositories with package information
Updating dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
  - Locking google/protobuf (v3.15.7)
  - Locking grpc/grpc (1.36.0)
Writing lock file
Installing dependencies from lock file (including require-dev)
Package operations: 2 installs, 0 updates, 0 removals
  - Downloading google/protobuf (v3.15.7)
  - Downloading grpc/grpc (1.36.0)
  - Installing google/protobuf (v3.15.7): Extracting archive
  - Installing grpc/grpc (1.36.0): Extracting archive
Generating autoload files

运行RPC服务器

从examples/node目录:

$ npm install
$ cd dynamic_codegen
$ node greeter_server.js

greeter_servier.js

var PROTO_PATH = __dirname + '/../../protos/helloworld.proto';

var grpc = require('@grpc/grpc-js');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
     longs: String,
     enums: String,
     defaults: true,
     oneofs: true
    });
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

/**
 * Implements the SayHello RPC method.
 */
function sayHello(call, callback) {
  callback(null, {message: 'Hello ' + call.request.name});
}

/**
 * Starts an RPC server that receives requests for the Greeter service at the
 * sample server port
 */
function main() {
  var server = new grpc.Server();
  server.addService(hello_proto.Greeter.service, {sayHello: sayHello});
  server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
    server.start();
  });
}

main();

执行RPC客户端

在另一个终端的examples/php目录中,运行客户端

$ ./run_greeter_client.sh

输出:

grpc/examples/php$ ./run_greeter_client.sh
PHP Warning:  Module 'grpc' already loaded in Unknown on line 0
Hello world

我们看run_greeter_client.sh的文件内容:

set -e
cd $(dirname $0)
php -d extension=grpc.so -d max_execution_time=300 \
  greeter_client.php $1

再看被执行的业务逻辑代码greeter_client.php:

require dirname(__FILE__).'/vendor/autoload.php';

function greet($hostname, $name)
{
    $client = new Helloworld\GreeterClient($hostname, [
        'credentials' => Grpc\ChannelCredentials::createInsecure(),
    ]);
    $request = new Helloworld\HelloRequest();
    $request->setName($name);
    list($response, $status) = $client->SayHello($request)->wait();
    if ($status->code !== Grpc\STATUS_OK) {
        echo "ERROR: " . $status->code . ", " . $status->details . PHP_EOL;
        exit(1);
    }
    echo $response->getMessage() . PHP_EOL;
}

$name = !empty($argv[1]) ? $argv[1] : 'world';
$hostname = !empty($argv[2]) ? $argv[2] : 'localhost:50051';
greet($hostname, $name);

其实我们已经在cli模式下php.ini中增加了grpc.so扩展,完全可以直接执行业务逻辑php脚本(rpc客户端):

grpc/examples/php$ php greeter_client.php 9ong.com
Hello 9ong.com

更新gRPC服务

  • 修改RPC服务端代码

    打开greeter_server.js,增加方法:(注意两个地方代码调整)

    var PROTO_PATH = __dirname + '/../../protos/helloworld.proto';
    
    var grpc = require('@grpc/grpc-js');
    var protoLoader = require('@grpc/proto-loader');
    var packageDefinition = protoLoader.loadSync(
        PROTO_PATH,
        {keepCase: true,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true
        });
    var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;
    
    /**
    * Implements the SayHello RPC method.
    */
    function sayHello(call, callback) {
        callback(null, {message: 'Hello ' + call.request.name});
    }
    //@9ong 代码调整1
    function sayHelloAgain(call, callback) {
        callback(null, {message: 'Hello again' + call.request.name});
    }
    
    
    /**
    * Starts an RPC server that receives requests for the Greeter service at the
    * sample server port
    */
    function main() {
    var server = new grpc.Server();
    //@9ong 代码调整2
    server.addService(hello_proto.Greeter.service, {sayHello: sayHello,sayHelloAgain: sayHelloAgain});
    server.bindAsync('0.0.0.0:50051', grpc.ServerCredentials.createInsecure(), () => {
        server.start();
    });
    }
    
    main();
    
    
  • 修改.proto服务定义文件

    更新helloworld.proto服务定义:

    // The greeting service definition.
    service Greeter {
        // Sends a greeting
        rpc SayHello (HelloRequest) returns (HelloReply) {}
        rpc SayHelloAgain (HelloRequest) returns (HelloReply) {}
    }
    
    
  • 重新生成RPC客户端stub classess

    /**
    * The greeting service definition.
    */
    class GreeterClient extends \Grpc\BaseStub {
    
        /**
        * @param string $hostname hostname
        * @param array $opts channel options
        * @param \Grpc\Channel $channel (optional) re-use channel object
        */
        public function __construct($hostname, $opts, $channel = null) {
            parent::__construct($hostname, $opts, $channel);
        }
    
        /**
        * Sends a greeting
        * @param \Helloworld\HelloRequest $argument input argument
        * @param array $metadata metadata
        * @param array $options call options
        * @return \Grpc\UnaryCall
        */
        public function SayHello(\Helloworld\HelloRequest $argument,
        $metadata = [], $options = []) {
            return $this->_simpleRequest('/helloworld.Greeter/SayHello',
            $argument,
            ['\Helloworld\HelloReply', 'decode'],
            $metadata, $options);
        }
    
        /**
        * @param \Helloworld\HelloRequest $argument input argument
        * @param array $metadata metadata
        * @param array $options call options
        * @return \Grpc\UnaryCall
        */
        public function SayHelloAgain(\Helloworld\HelloRequest $argument,
        $metadata = [], $options = []) {
            return $this->_simpleRequest('/helloworld.Greeter/SayHelloAgain',
            $argument,
            ['\Helloworld\HelloReply', 'decode'],
            $metadata, $options);
        }
    
    }
    
    
  • 测试执行

    业务逻辑代码greeter_client.php:

    function greet($hostname, $name)
    {
        $client = new Helloworld\GreeterClient($hostname, [
            'credentials' => Grpc\ChannelCredentials::createInsecure(),
        ]);
        $request = new Helloworld\HelloRequest();
        $request->setName($name);
        list($response, $status) = $client->SayHello($request)->wait();
        if ($status->code !== Grpc\STATUS_OK) {
            echo "ERROR: " . $status->code . ", " . $status->details . PHP_EOL;
            exit(1);
        }
        echo $response->getMessage() . PHP_EOL;
    
        list($reply, $status) = $client->SayHelloAgain($request)->wait();
        echo $reply->getMessage().PHP_EOL;
    }
    
    

    执行:

    grpc/examples/php$ php greeter_client.php
    Hello world
    Hello againworld
    
    

参考

Basics tutorial | PHP | gRPC

Quick start | Node | gRPC