程序员程序园微服务架构和实践

tech| 再探 grpc

2019-04-30  本文已影响165人  daydaygo

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

折腾 grpc 过几次, 都没有大规模的用起来, 熟悉程度多停留在官网的 helloworld 上, 对原理的理解不够深入, 所以经常会卡住.

grpc| python 实战 grpc

这里有介绍过我 卡住 的点, 按照官网的 quick start 文档:

来自 PHPer 的灵魂叩问: 要么搞定环境, 要么用不了 grpc ?

就是陷入到这个问题里去了, 一直绕不出来. 但是理解了 grpc 基本原理, 换个思路, 就会发现非常的简单.

官方文档的解读

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

官方 php quickstart 介绍的步骤:

有 2 点容易让人产生误读的地方:

理解 grpc

从几个基础的点, 一点一点来看 grpc.

protobuf

protobuf 环境:

通过时序来理解:

补充一点, 信息的序列化/反序列化, 就涉及到编码的知识, 包括: 进制转换 -> 字符集(为什么会乱码) -> 大端序/小端序/网络序(php pack()/unpack() 函数)

具体到 PHP 中, 以官网的 helloworld 为例子:

syntax = "proto3";

package grpc;

service HelloService {
    rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
    string greeting = 1;
}

message HelloResponse {
    string reply = 1;
}
# alpine linux 为例, 其他 linux 发行版, 使用相应包管理工具安装
apk add protobuf
protoc --version # 验证 protoc 是否安装成功

# 使用 protoc 生成代码
protoc --php_out=grpc/ game.proto # 使用 --php_out 选项, 指定生成 PHP 代码的路径

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 层网络通信:

为什么需要协议: tcp 是流式(stream)传输数据的, 需要协议来确定数据边界
简单协议设计: EOF结束符 / 固定包头

swoole wiki - 网络通信协议设计: https://wiki.swoole.com/wiki/page/484.html

有了 swoole, tcp 通信, 编程十分简单:

<?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();
<?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));
    }
});

这里包括四部分:

server 的示例代码有了, client 也可以使用 swoole http2 协程 client 相应封装了

<?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:

补充 && 更多

更多:

上一篇下一篇

猜你喜欢

热点阅读