tech| 再探 grpc

作者: daydaygo | 来源:发表于2019-04-30 15:00 被阅读165次

    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 的

    相关文章

      网友评论

        本文标题:tech| 再探 grpc

        本文链接:https://www.haomeiwen.com/subject/vvqvnqtx.html