谷歌的 GRPC 目前對於 PHP 沒有 server 實作。之前順教學文件的時候順得很開心結果到後面突然冒出接下來我們用 node 起個 server,看得我滿臉問號...(現在改掉了,開頭就跟你說 PHP 目前沒有server支援)。
但是我就有現存的 PHP library 想接啊,要我如何是好。好在網路總有很多好心人士提供支援: spiral/php-grpc ,用 golang 實作了可以跑 PHP 的 GRPC server。姑且紀錄一下做了什麼事情。
GRPC 程式開發流程
GRPC 程式的開發基本上就是:
- 用 protobuf 定義出介面
- 用工具將 protobuf 轉換為要實作的程式語言
- 在你選定的語言實做你的邏輯
- 用(和上面那個工具)相對應的環境把程式跑起來
我們需要的工具
如上所述,所以我們需要有轉換的工具和對應跑得起來的環境。在這裡我們需要有:
- protoc 、grpc_php_plugin:Google 官方所提供的 protobuf compiler 以及 PHP plugin。可以跟著 README 自己編譯 編出完整的工具,或是直接下
make grpc_php_plugin
只會編出這兩個東西。 - grpc php plugin :Google 官方提供的 grpc php runtime,pecl 是你的好朋友。
- protoc-gen-php-grpc :spiral/php-grpc 所提供的 protobuf compiler
- rr-grpc :spiral/php-grpc 的 server,即是上述對應跑得起來的環境。2.3 兩包可以直接在 spiral/php-grpc 的 release 找到 prebuild binary。
- protobuf php extension (or composer package google/protobuf)
動手做
定義 protobuf
總之來定義一組 GRPC 吧。我們這個 Example Service 接受2個數字和一個運算符,回傳結果給人家,姑且稱他為 example.proto:
syntax = "proto3";package poyu.grpc;service ExampleService {
rpc Calculate(ExampleRequest) returns (ExampleResponse) {}
}message ExampleRequest {
uint64 a = 1;
uint64 b = 2;
string op = 3;
}message ExampleResponse {
uint64 c = 1;
}
- 第一行的
syntax = “proto3”
,定義了我們使用的語法版本。 package
就是 namespace 的概念。service
可以視為一個介面,對應到 php 大概是這種感覺:
interface ExampleService {
function Calculate(ExampleRequest req) : ExampleResponse;
}
- message 則可以視為一個 struct。一個詭異的地方就是對 message 裡面每個 property 都需要指定一個流水數自給他。另外就是 rpc 的 input/output 只能是 message (你不能直接丟一個 int 給他)。
各種資料型態以及規範可以直接參閱文件。
實作 server
將 protobuf 轉換成 PHP code:
$ /path/to/your/protoc example.proto --php_out=src --grpc_out=src --plugin=protoc-gen-grpc=/path/to/your/protoc-gen-php-grpc
幾個參數的意思大概如下:
- example.proto :你的 protobuf file
- --php_out :protoc 將你的 protobuf 轉換出來的 PHP code 的輸出位置
- --grpc_out :proto-gen-grpc 將 protobuf 轉換出來的 code 的輸出位置
- --plugin :設定 proto-gen-grpc 到底是執行誰
這個看起來有點莫名其妙,為何 protoc-gen-php-grpc 又突然搖身一變變成 protoc 的 plugin。把指令拆成兩次下就可以知道箇中奇妙之處:
$ protoc example.proto --php_out=src
$ tree src
src/
├── GPBMetadata
│ └── Example.php
└── Poyu
└── Grpc
├── ExampleRequest.php
└── ExampleResponse.php$ protoc example.proto --grpc_out=src
protoc-gen-grpc: program not found or is not executable
--grpc_out: protoc-gen-grpc: Plugin failed with status code 1.$ protoc example.proto --grpc_out=src --plugin=protoc-gen-grpc=/path/to/protoc-gen-php-grpc
$ tree src/
src/
├── GPBMetadata
│ └── Example.php
└── Poyu
└── Grpc
├── ExampleRequest.php
├── ExampleResponse.php
└── ExampleServiceInterface.php
真相大白,protoc 本身的能力只有產出 stub,也就是 protobuf 在這個語言裡的資料結構。而實際上這個資料結構怎麼在這個語言內透過 grpc 的介面來傳遞則是由實作的人進行處理。
材料都準備好了,可以開始準備實作。
1. 首先先安裝 spiral/php-grpc 的 PHP package:
$ composer require spiral/php-grpc
2. 把剛剛產出來的 code 加進 autoload
3. 實作 ExampleServiceInterface
首先看看 ExampleServiceInterface 糾竟要實作些什麼東西:
public function Calculate(GRPC\ContextInterface $ctx, ExampleRequest $in): ExampleResponse;
也不意外的就是要時作出剛剛定義的 Calculate 函式,這個函式接收一個 context 和一個 ExampleRequest,回傳一個 ExampleResponse。context 在目前還用不到姑且先不去研究,問題是要怎麼從 ExampleRequset 拿出資料和把資料塞回 ExampleResponse。
其實看一下也不難,在產出的物件中提供了所有剛剛定義欄位的 getter 和 setter。而要建立物件時也可以直接將對應的 array 放進建構元直接產生。所以寫起來流程大概就是先解出參數、工作、再包裝成物件回傳:
class ExampleService implements ExampleServiceInterface
{
public function Calculate(ContextInterface $ctx, ExampleRequest $in): ExampleResponse
{
$a = $in->getA();
$b = $in->getB();
$op = $in->getOp(); switch ($op) {
case '+':
$result = $a + $b;
break;
// 其他 op 省略...
} return new ExampleResponse([
'c' => $result
]);
}
}
4. 實作 worker.php
<?phprequire_once __DIR__ . '/vendor/autoload.php';$server = new Spiral\GRPC\Server();
$server->registerService(Poyu\Grpc\ExampleServiceInterface::class, new Poyu\Service\ExampleService());$worker = new Spiral\RoadRunner\Worker(new Spiral\Goridge\StreamRelay(STDIN, STDOUT));$server->serve($worker);
用別人的東西基本上就是照著別人的規則來玩,流程上就是設定 Server 中 Service 的對應,然後讓 Worker 跑起來。
如果要再求甚解一些,可以再往下追 spiral/goridge、spiral/roadrunner。前者定義並實做了 PHP 與 GO 的 RPC 介面、後者則是處理 HTTP 並透過前者轉化成 PHP PSR7 介面來給 PHP 處理。
大概有上面的認知再回來看上面的 code 會比較可以理解他在幹嘛。(所以我們會發現,因為他用 STDIO 來做溝通,所以印 debug 訊息你就永遠都跑不結果:P)。
5. 設定 rr-grpc
grpc:
listen: "tcp://:5566"
proto: "example.proto"
workers:
command: "php worker.php"
pool:
numWorkers: 4
設定 rr-grpc 啟動的參數,他預設會去吃 .rr.yaml
。這裡我們設定成把 server 開在 5566 port,然後起4個 worker。基本上他就是由 roadrunner 擴展而來,可以參考相關文件。
6. 啟動 server
$ /path/to/rr-grpc serve -v -d
實作 client
將 protobuf 轉換為 PHP code:
$ /path/to/your/protoc example.proto --php_out=src --grpc_out=src --plugin=protoc-gen-grpc=/path/to/your/grpc_php_plugin$ tree src/
src/
├── GPBMetadata
│ └── Example.php
└── Poyu
└── Grpc
├── ExampleRequest.php
├── ExampleResponse.php
└── ExampleServiceClient.php
而這裡產出來可以看到由 grpc_php_plugin 產出了 php client 專用的 ExampleServiceClient.php 。
接著就可以開始實作。
- 首先先安裝 grpc 的 php package。
$ composer require grpc/grpc
2. 把產出的 code 加進 autoload
3. 寫個 main.php 來呼叫他。
<?phprequire_once __DIR__ . '/vendor/autoload.php';use Poyu\Grpc\ExampleServiceClient;
use Poyu\Grpc\ExampleRequest;$client = new ExampleServiceClient('127.0.0.1:5566', [
'credentials' => Grpc\ChannelCredentials::createInsecure(),
]);list($response, $status) = $client->Calculate(new ExampleRequest([
'a' => 1,
'b' => 2,
'op' => '+',
]))->wait();var_export($status);
var_export($response->getC());
client 基本上就比較沒有什麼眉角,new 出一個 client 設定連線資訊,接著呼叫函數等待回傳。
如果沒有任何手滑,這時我們就會看到 server 的那個畫面有 log 出現,而client 這邊最後面可以得到 3。
有機會投產再來深入研究。完整 DEMO CODE 在此。