date: 2019-04-25 22:16:01
title: tech| 再探 grpc
折騰 grpc 過幾次, 都沒有大規(guī)模的用起來, 熟悉程度多停留在官網(wǎng)的 helloworld 上, 對(duì)原理的理解不夠深入, 所以經(jīng)常會(huì)卡住.
這里有介紹過我 卡住 的點(diǎn), 按照官網(wǎng)的 quick start 文檔:
- 使用 php: 配置 PHP 的環(huán)境麻煩, 尤其
grpc/grpc
代碼庫(kù)編譯出grpc_php_plugin
這一步 - 使用 go: 安裝 golang 包, 經(jīng)常 撞墻(go get 失敗)
- 最后 偷懶 使用 python 跑了一遍, 最大的收獲是 grpc 除了 單向請(qǐng)求, 還有 雙向通信(stream, 流式通信), 把環(huán)境的問題繞過去后跑通了 demo
來自 PHPer 的靈魂叩問: 要么搞定環(huán)境, 要么用不了 grpc ?
就是陷入到這個(gè)問題里去了, 一直繞不出來. 但是理解了 grpc 基本原理, 換個(gè)思路, 就會(huì)發(fā)現(xiàn)非常的簡(jiǎn)單.
官方文檔的解讀
grpc - quickstart - php: https://grpc.io/docs/quickstart/php/
官方 php quickstart 介紹的步驟:
- grpc 環(huán)境
- ext-grpc
-
github.com/grpc/grpc
源碼庫(kù)中編譯出grpc_php_plugin
, 此擴(kuò)展用來配合 protoc, 來自動(dòng)生成代碼
- protobuf 環(huán)境
- proto 文件, 基于 IDL 文件定義服務(wù), 目前使用 proto3 語(yǔ)法(語(yǔ)法很簡(jiǎn)單, 一刻鐘內(nèi)就可以看完)
- protoc, protobuf compile, proto 文件編譯器, 可以理解 proto 文件基于不同開發(fā)語(yǔ)言進(jìn)行 翻譯
- protobuf runtime, protobuf 格式的運(yùn)行時(shí)支持, protobuf 序列化后的信息, 需要 protobuf runtime
有 2 點(diǎn)容易讓人產(chǎn)生誤讀的地方:
- 順序: 先理解了 protobuf 環(huán)境, 進(jìn)一步再來構(gòu)建 grpc
- 官網(wǎng)自動(dòng)生成的代碼, 只是能跑通 grpc 服務(wù)調(diào)用. 但現(xiàn)實(shí)是, rpc 服務(wù), 需要一整套的服務(wù)框架進(jìn)行支持, 比如說: 微服務(wù)
理解 grpc
從幾個(gè)基礎(chǔ)的點(diǎn), 一點(diǎn)一點(diǎn)來看 grpc.
- protobuf: 序列化, 編碼的基礎(chǔ)知識(shí)
- rpc, tcp 基礎(chǔ)上的通信: tcp 通信為什么需要協(xié)議, 協(xié)議設(shè)計(jì)簡(jiǎn)單
- grpc 的通信協(xié)議細(xì)節(jié)
protobuf
protobuf 環(huán)境:
- proto 文件, 基于 IDL 文件定義服務(wù), 目前使用 proto3 語(yǔ)法(語(yǔ)法很簡(jiǎn)單, 一刻鐘內(nèi)就可以看完)
- protoc, protobuf compile, proto 文件編譯器, 可以理解 proto 文件基于不同開發(fā)語(yǔ)言進(jìn)行 翻譯
- protobuf runtime, protobuf 格式的運(yùn)行時(shí)支持, protobuf 序列化后的信息, 需要 protobuf runtime
通過時(shí)序來理解:
- proto 文件 -> protc 編譯 -> 自動(dòng)生成不同語(yǔ)言的代碼(gen code)
- gen code + protobuf runtime -> 信息序列化/反序列化
補(bǔ)充一點(diǎn), 信息的序列化/反序列化, 就涉及到編碼的知識(shí), 包括: 進(jìn)制轉(zhuǎn)換 -> 字符集(為什么會(huì)亂碼) -> 大端序/小端序/網(wǎng)絡(luò)序(php pack()/unpack() 函數(shù))
具體到 PHP 中, 以官網(wǎng)的 helloworld 為例子:
- proto 文件
syntax = "proto3";
package grpc;
service HelloService {
rpc SayHello (HelloRequest) returns (HelloResponse);
}
message HelloRequest {
string greeting = 1;
}
message HelloResponse {
string reply = 1;
}
- protoc
# alpine linux 為例, 其他 linux 發(fā)行版, 使用相應(yīng)包管理工具安裝
apk add protobuf
protoc --version # 驗(yàn)證 protoc 是否安裝成功
# 使用 protoc 生成代碼
protoc --php_out=grpc/ game.proto # 使用 --php_out 選項(xiàng), 指定生成 PHP 代碼的路徑
- protobuf runtime
PHP 中其實(shí)很簡(jiǎn)單 ext-protobuf / google/protobuf package
, 二選一
// ext-protobuf
pecl install protobuf
// google/protobuf
composer require google/protobuf
到這里, 就把 protobuf 這部分的內(nèi)容都解決了, 下面是生成的例子
// proto
message HelloRequest {
string greeting = 1;
}
<?php
# Generated by the protocol buffer compiler. DO NOT EDIT!
# source: hello.proto
namespace Grpc;
use Google\Protobuf\Internal\GPBType;
use Google\Protobuf\Internal\RepeatedField;
use Google\Protobuf\Internal\GPBUtil;
/**
* Generated from protobuf message <code>grpc.HelloRequest</code>
*/
class HelloRequest extends \Google\Protobuf\Internal\Message
{
/**
* Generated from protobuf field <code>string greeting = 1;</code>
*/
private $greeting = '';
public function __construct() {
\GPBMetadata\Hello::initOnce();
parent::__construct();
}
/**
* Generated from protobuf field <code>string greeting = 1;</code>
* @return string
*/
public function getGreeting()
{
return $this->greeting;
}
/**
* Generated from protobuf field <code>string greeting = 1;</code>
* @param string $var
* @return $this
*/
public function setGreeting($var)
{
GPBUtil::checkString($var, True);
$this->greeting = $var;
return $this;
}
}
rpc, tcp 基礎(chǔ)上的通信
tcp/ip 4 層網(wǎng)絡(luò)通信:
- 物理層/數(shù)據(jù)鏈路層: 網(wǎng)線/路由器/交換機(jī)/網(wǎng)卡 -> mac地址
- ip 層: ip 地址, 4 種網(wǎng)絡(luò)地址類型
- tcp/udp層: 端口, 端口上綁定的服務(wù)
- 協(xié)議層: 各種熟悉的協(xié)議, http/ftp
為什么需要協(xié)議: tcp 是流式(stream)傳輸數(shù)據(jù)的, 需要協(xié)議來確定數(shù)據(jù)邊界
簡(jiǎn)單協(xié)議設(shè)計(jì): EOF結(jié)束符 / 固定包頭
swoole wiki - 網(wǎng)絡(luò)通信協(xié)議設(shè)計(jì): https://wiki.swoole.com/wiki/page/484.html
有了 swoole, tcp 通信, 編程十分簡(jiǎn)單:
- server.php: tcp 協(xié)程 server
<?php
use Swoole\Server;
// swoole>=v4.0 開始默認(rèn)開啟協(xié)程
$s = new Server('0.0.0.0', '9502', SWOOLE_BASE, SWOOLE_TCP);
$s->set([
'worker_num' => 4,
'daemonize' => true,
'backlog' => 128,
]);
$s->on('connect', 'on_connect');
$s->on('receive', 'on_receive');
$s->on('close', 'on_close');
$s->start();
- client.php: tcp 協(xié)程 client
<?php
use Swoole\Coroutine\Client;
$c = new Client(SWOOLE_SOCK_TCP);
$c->connect('127.0.0.1', '9502');
$c->send('hello');
echo $c->recv();
$c->close();
- 加上協(xié)議處理, 簡(jiǎn)單的協(xié)議只需要修改配置就可以實(shí)現(xiàn)
<?php
use Swoole\Coroutine\Client;
$c = new Client(SWOOLE_SOCK_TCP);
// 協(xié)議處理
$client->set([
'open_length_check' => 1,
'package_length_type' => 'N',
'package_length_offset' => 0, //第N個(gè)字節(jié)是包長(zhǎng)度的值
'package_body_offset' => 4, //第幾個(gè)字節(jié)開始計(jì)算長(zhǎng)度
'package_max_length' => 2000000, //協(xié)議最大長(zhǎng)度
]);
$c->connect('127.0.0.1', '9502');
$c->send('hello');
echo $c->recv();
$c->close();
grpc = http2 + protobuf
grpc 基于 http2 協(xié)議進(jìn)行通信, 理解上面的基礎(chǔ)知識(shí), 再來看 grpc 使用的 http2 協(xié)議通信細(xì)節(jié), 完全可以簡(jiǎn)單實(shí)現(xiàn):
<?php
$http = new \Swoole\Http\Server('0.0.0.0', 9501);
$http->set([
'open_http2_protocol' => true,
]);
$http->on('workerStart', function (\Swoole\Http\Server $server) {
echo "workerStart \n";
});
$http->on('request', function (\Swoole\Http\Request $request, \Swoole\Http\Response $response) {
// request_uri 和 proto 文件中 rpc 對(duì)應(yīng)關(guān)系: /{package}.{service}/{rpc}
$path = $request->server['request_uri'];
if ($path == '/grpc.HelloService/SayHello') {
// decode, 獲取 rpc 中的請(qǐng)求
$request_message = \Grpc\Parser::deserializeMessage([HelloRequest::class, null], $request->rawContent());
// encode, 返回 rpc 中的應(yīng)答
$response_message = new HelloReply();
$response_message->setMessage('Hello ' . $request_message->getName());
$response->header('content-type', 'application/grpc');
$response->header('trailer', 'grpc-status, grpc-message');
$trailer = [
"grpc-status" => "0",
"grpc-message" => ""
];
foreach ($trailer as $trailer_name => $trailer_value) {
$response->trailer($trailer_name, $trailer_value);
}
$response->end(\Grpc\Parser::serializeMessage($response_message));
}
});
這里包括四部分:
-
\Swoole\Http\Server
: 使用 swoole 實(shí)現(xiàn)的 http2 server - .proto 文件中定義的 grpc 服務(wù)名:
request_uri 和 proto 文件中 rpc 對(duì)應(yīng)關(guān)系: /{package}.{service}/{rpc}
-
\Grpc\Parser
: grpc 信息的解析類, 根據(jù) grpc 使用的 http2 協(xié)議細(xì)節(jié)封裝一個(gè)類就 搞定了 - HelloRequest / HelloReply: .ptoto 文件 + protoc 自動(dòng)生成的 protobuf 自動(dòng)解析文件
server 的示例代碼有了, client 也可以使用 swoole http2 協(xié)程 client 相應(yīng)封裝了
-
\Grpc\Parser
示例代碼:
<?php
namespace Grpc;
use Google\Protobuf\Internal\Message;
class Parser
{
public static function pack(string $data): string
{
return $data = pack('CN', 0, strlen($data)) . $data;
}
public static function unpack(string $data): string
{
return $data = substr($data, 5);
}
public static function serializeMessage($data)
{
if (method_exists($data, 'encode')) {
$data = $data->encode();
} else if (method_exists($data, 'serializeToString')) {
$data = $data->serializeToString();
} else {
/** @noinspection PhpUndefinedMethodInspection */
$data = $data->serialize();
}
return self::pack($data);
}
public static function deserializeMessage($deserialize, string $value)
{
if (empty($value)) {
return null;
} else {
$value = self::unpack($value);
}
if (is_array($deserialize)) {
list($className, $deserializeFunc) = $deserialize;
/** @var $obj Message */
$obj = new $className();
if ($deserializeFunc && method_exists($obj, $deserializeFunc)) {
$obj->$deserializeFunc($value);
} else {
$obj->mergeFromString($value);
}
return $obj;
}
return call_user_func($deserialize, $value);
}
public static function parseToResultArray($response, $deserialize): array
{
if (!$response) {
return ['No response', GRPC_ERROR_NO_RESPONSE, $response];
} else if ($response->statusCode !== 200) {
return ['Http status Error', $response->errCode ?: $response->statusCode, $response];
} else {
$grpc_status = (int)($response->headers['grpc-status'] ?? 0);
if ($grpc_status !== 0) {
return [$response->headers['grpc-message'] ?? 'Unknown error', $grpc_status, $response];
}
$data = $response->data;
$reply = self::deserializeMessage($deserialize, $data);
$status = (int)($response->headers['grpc-status'] ?? 0 ?: 0);
return [$reply, $status, $response];
}
}
}
寫在最后
到這里, 基本上 grpc 的簡(jiǎn)單原理, 都在上面寫的例子中展示出來了, 能將自己以前積累的知識(shí)融會(huì)貫通起來, 喜悅之情噴涌而出!
值得一提的點(diǎn)
一開始卡住就是拋開原理跑 demo, 不斷在折騰環(huán)境, 折騰代碼自動(dòng)生成, 跑官網(wǎng) demo 上越走越遠(yuǎn). 之前遇到的一個(gè)例子再提一下, 希望能有所啟發(fā).
alipay ILLEGAL_SIGN 錯(cuò)誤解決: http://www.reibang.com/p/28585a6454b2
整個(gè)調(diào)用鏈路非常長(zhǎng), debug 問題的時(shí)候前前后后 trace 了很久, 盡其所能的做了各種嘗試, 但是回歸到本質(zhì): http 協(xié)議
所以赁严,翻開了《http 權(quán)威指南》昂羡,仔細(xì)查閱之后,你就會(huì)發(fā)現(xiàn)渴语,在 http協(xié)議里面怔揩,只有 2 個(gè)地方會(huì)影響到 charset:
- 客戶端:
accept-charset='utf-8'
- 服務(wù)端:
content-type: text/plain;charset:utf-8
補(bǔ)充 && 更多
-
swoole/grpc
: 可以參考的項(xiàng)目, 推薦只看swoole/grpc/examples/grpc/greeter_server.php
和swoole/grpc/examples/grpc/greeter_client.php
- 關(guān)于 protobuf 的實(shí)踐, 我之前的 blog: 服務(wù)器開發(fā)系列 1
- nginx 對(duì) grpc 的支持: https://www.nginx.com/blog/nginx-1-13-10-grpc/
- 極客時(shí)間 - 深入淺出 grpc: https://time.geekbang.org/column/intro/75
更多:
- grpc 序列化機(jī)制(protobuf) && grpc 安全性設(shè)計(jì)
- 我是如何在 swoft2 中輕松使用 grpc 的