社区微信群开通啦,扫一扫抢先加入社区官方微信群
社区微信群
date: 2019-04-25 22:16:01
title: tech| 再探 grpc
折腾 grpc 过几次, 都没有大规模的用起来, 熟悉程度多停留在官网的 helloworld 上, 对原理的理解不够深入, 所以经常会卡住.
这里有介绍过我 卡住 的点, 按照官网的 quick start 文档:
grpc/grpc
代码库编译出 grpc_php_plugin
这一步来自 PHPer 的灵魂叩问: 要么搞定环境, 要么用不了 grpc ?
就是陷入到这个问题里去了, 一直绕不出来. 但是理解了 grpc 基本原理, 换个思路, 就会发现非常的简单.
grpc - quickstart - php: https://grpc.io/docs/quickstart/php/
官方 php quickstart 介绍的步骤:
github.com/grpc/grpc
源码库中编译出 grpc_php_plugin
, 此扩展用来配合 protoc, 来自动生成代码有 2 点容易让人产生误读的地方:
从几个基础的点, 一点一点来看 grpc.
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 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;
}
}
tcp/ip 4 层网络通信:
为什么需要协议: tcp 是流式(stream)传输数据的, 需要协议来确定数据边界
简单协议设计: EOF结束符 / 固定包头
swoole wiki - 网络通信协议设计: https://wiki.swoole.com/wiki/page/484.html
有了 swoole, tcp 通信, 编程十分简单:
<?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();
<?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 协议进行通信, 理解上面的基础知识, 再来看 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 serverrequest_uri 和 proto 文件中 rpc 对应关系: /{package}.{service}/{rpc}
GrpcParser
: grpc 信息的解析类, 根据 grpc 使用的 http2 协议细节封装一个类就 搞定了
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
swoole/grpc
: 可以参考的项目, 推荐只看 swoole/grpc/examples/grpc/greeter_server.php
和 swoole/grpc/examples/grpc/greeter_client.php
更多:
如果觉得我的文章对您有用,请随意打赏。你的支持将鼓励我继续创作!