tech| 再探 grpc - Go语言中文社区

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 GoogleProtobufInternalGPBType;
use GoogleProtobufInternalRepeatedField;
use GoogleProtobufInternalGPBUtil;

/**
 * Generated from protobuf message <code>grpc.HelloRequest</code>
 */
class HelloRequest extends GoogleProtobufInternalMessage
{
    /**
     * Generated from protobuf field <code>string greeting = 1;</code>
     */
    private $greeting = '';

    public function __construct() {
        GPBMetadataHello::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 SwooleServer;

// 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 SwooleCoroutineClient;

$c = new Client(SWOOLE_SOCK_TCP);
$c->connect('127.0.0.1', '9502');
$c->send('hello');
echo $c->recv();
$c->close();
  • 加上协议处理, 简单的协议只需要修改配置就可以实现
<?php

use SwooleCoroutineClient;

$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 SwooleHttpServer('0.0.0.0', 9501);
$http->set([
    'open_http2_protocol' => true,
]);
$http->on('workerStart', function (SwooleHttpServer $server) {
    echo "workerStart n";
});
$http->on('request', function (SwooleHttpRequest $request, SwooleHttpResponse $response) {
    // request_uri 和 proto 文件中 rpc 对应关系: /{package}.{service}/{rpc}
    $path = $request->server['request_uri'];

    if ($path == '/grpc.HelloService/SayHello') {
        // decode, 获取 rpc 中的请求
        $request_message = GrpcParser::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(GrpcParser::serializeMessage($response_message));
    }
});

这里包括四部分:

  • SwooleHttpServer: 使用 swoole 实现的 http2 server
  • .proto 文件中定义的 grpc 服务名: request_uri 和 proto 文件中 rpc 对应关系: /{package}.{service}/{rpc}
  • GrpcParser: grpc 信息的解析类, 根据 grpc 使用的 http2 协议细节封装一个类就 搞定了
  • HelloRequest / HelloReply: .ptoto 文件 + protoc 自动生成的 protobuf 自动解析文件

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

  • GrpcParser 示例代码:
<?php

namespace Grpc;

use GoogleProtobufInternalMessage;

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 的
版权声明:本文来源简书,感谢博主原创文章,遵循 CC 4.0 by-sa 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://www.jianshu.com/p/f3221df39e6f
站方申明:本站部分内容来自社区用户分享,若涉及侵权,请联系站方删除。
  • 发表于 2020-02-02 14:22:06
  • 阅读 ( 1243 )
  • 分类:

0 条评论

请先 登录 后评论

官方社群

GO教程

猜你喜欢