tech| 再探 grpc

date: 2019-04-25 22:16:01
title: tech| 再探 grpc

折騰 grpc 過幾次, 都沒有大規模的用起來, 熟悉程度多停留在官網的 helloworld 上, 對原理的理解不夠深入, 所以經常會卡住.

grpc| python 實戰 grpc

這裏有介紹過我 卡住 的點, 按照官網的 quick start 文檔:

  • 使用 php: 配置 PHP 的環境麻煩, 尤其 grpc/grpc 代碼庫編譯出 grpc_php_plugin 這一步
  • 使用 go: 安裝 golang 包, 經常 撞牆(go get 失敗)
  • 最後 偷懶 使用 python 跑了一遍, 最大的收穫是 grpc 除了 單向請求, 還有 雙向通信(stream, 流式通信), 把環境的問題繞過去後跑通了 demo

來自 PHPer 的靈魂叩問: 要麼搞定環境, 要麼用不了 grpc ?

就是陷入到這個問題裏去了, 一直繞不出來. 但是理解了 grpc 基本原理, 換個思路, 就會發現非常的簡單.

官方文檔的解讀

grpc - quickstart - php: https://grpc.io/docs/quickstart/php/

官方 php quickstart 介紹的步驟:

  • grpc 環境
    • ext-grpc
    • github.com/grpc/grpc 源碼庫中編譯出 grpc_php_plugin, 此擴展用來配合 protoc, 來自動生成代碼
  • protobuf 環境
    • proto 文件, 基於 IDL 文件定義服務, 目前使用 proto3 語法(語法很簡單, 一刻鐘內就可以看完)
    • protoc, protobuf compile, proto 文件編譯器, 可以理解 proto 文件基於不同開發語言進行 翻譯
    • protobuf runtime, protobuf 格式的運行時支持, protobuf 序列化後的信息, 需要 protobuf runtime

有 2 點容易讓人產生誤讀的地方:

  • 順序: 先理解了 protobuf 環境, 進一步再來構建 grpc
  • 官網自動生成的代碼, 只是能跑通 grpc 服務調用. 但現實是, rpc 服務, 需要一整套的服務框架進行支持, 比如說: 微服務

理解 grpc

從幾個基礎的點, 一點一點來看 grpc.

  • protobuf: 序列化, 編碼的基礎知識
  • rpc, tcp 基礎上的通信: tcp 通信爲什麼需要協議, 協議設計簡單
  • grpc 的通信協議細節

protobuf

protobuf 環境:

  • proto 文件, 基於 IDL 文件定義服務, 目前使用 proto3 語法(語法很簡單, 一刻鐘內就可以看完)
  • protoc, protobuf compile, proto 文件編譯器, 可以理解 proto 文件基於不同開發語言進行 翻譯
  • protobuf runtime, protobuf 格式的運行時支持, protobuf 序列化後的信息, 需要 protobuf runtime

通過時序來理解:

  • proto 文件 -> protc 編譯 -> 自動生成不同語言的代碼(gen code)
  • gen code + protobuf runtime -> 信息序列化/反序列化

補充一點, 信息的序列化/反序列化, 就涉及到編碼的知識, 包括: 進制轉換 -> 字符集(爲什麼會亂碼) -> 大端序/小端序/網絡序(php pack()/unpack() 函數)

具體到 PHP 中, 以官網的 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 發行版, 使用相應包管理工具安裝
apk add protobuf
protoc --version # 驗證 protoc 是否安裝成功

# 使用 protoc 生成代碼
protoc --php_out=grpc/ game.proto # 使用 --php_out 選項, 指定生成 PHP 代碼的路徑
  • protobuf runtime

PHP 中其實很簡單 ext-protobuf / google/protobuf package, 二選一

// ext-protobuf
pecl install protobuf

// google/protobuf
composer require google/protobuf

到這裏, 就把 protobuf 這部分的內容都解決了, 下面是生成的例子

// 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 基礎上的通信

tcp/ip 4 層網絡通信:

  • 物理層/數據鏈路層: 網線/路由器/交換機/網卡 -> mac地址
  • ip 層: ip 地址, 4 種網絡地址類型
  • tcp/udp層: 端口, 端口上綁定的服務
  • 協議層: 各種熟悉的協議, http/ftp

爲什麼需要協議: tcp 是流式(stream)傳輸數據的, 需要協議來確定數據邊界
簡單協議設計: EOF結束符 / 固定包頭

swoole wiki - 網絡通信協議設計: https://wiki.swoole.com/wiki/page/484.html

有了 swoole, tcp 通信, 編程十分簡單:

  • server.php: tcp 協程 server
<?php

use Swoole\Server;

// swoole>=v4.0 開始默認開啓協程
$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 協程 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();
  • 加上協議處理, 簡單的協議只需要修改配置就可以實現
<?php

use Swoole\Coroutine\Client;

$c = new Client(SWOOLE_SOCK_TCP);
// 協議處理
$client->set([
    'open_length_check'     => 1,
    'package_length_type'   => 'N',
    'package_length_offset' => 0,       //第N個字節是包長度的值
    'package_body_offset'   => 4,       //第幾個字節開始計算長度
    'package_max_length'    => 2000000,  //協議最大長度
]);
$c->connect('127.0.0.1', '9502');
$c->send('hello');
echo $c->recv();
$c->close();

grpc = http2 + protobuf

grpc 基於 http2 協議進行通信, 理解上面的基礎知識, 再來看 grpc 使用的 http2 協議通信細節, 完全可以簡單實現:

<?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 對應關係: /{package}.{service}/{rpc}
    $path = $request->server['request_uri'];

    if ($path == '/grpc.HelloService/SayHello') {
        // decode, 獲取 rpc 中的請求
        $request_message = \Grpc\Parser::deserializeMessage([HelloRequest::class, null], $request->rawContent());

        // encode, 返回 rpc 中的應答
        $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 實現的 http2 server
  • .proto 文件中定義的 grpc 服務名: request_uri 和 proto 文件中 rpc 對應關係: /{package}.{service}/{rpc}
  • \Grpc\Parser: grpc 信息的解析類, 根據 grpc 使用的 http2 協議細節封裝一個類就 搞定了
  • HelloRequest / HelloReply: .ptoto 文件 + protoc 自動生成的 protobuf 自動解析文件

server 的示例代碼有了, client 也可以使用 swoole http2 協程 client 相應封裝了

  • \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 的簡單原理, 都在上面寫的例子中展示出來了, 能將自己以前積累的知識融會貫通起來, 喜悅之情噴涌而出!

值得一提的點

一開始卡住就是拋開原理跑 demo, 不斷在折騰環境, 折騰代碼自動生成, 跑官網 demo 上越走越遠. 之前遇到的一個例子再提一下, 希望能有所啓發.

alipay ILLEGAL_SIGN 錯誤解決: https://www.jianshu.com/p/28585a6454b2

整個調用鏈路非常長, debug 問題的時候前前後後 trace 了很久, 盡其所能的做了各種嘗試, 但是迴歸到本質: http 協議

所以,翻開了《http 權威指南》,仔細查閱之後,你就會發現,在 http協議裏面,只有 2 個地方會影響到 charset:

  • 客戶端:accept-charset='utf-8'
  • 服務端:content-type: text/plain;charset:utf-8

補充 && 更多

更多:

  • grpc 序列化機制(protobuf) && grpc 安全性設計
  • 我是如何在 swoft2 中輕鬆使用 grpc 的
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章