Giter Club home page Giter Club logo

grpc's Introduction

Swoole-Grpc-Client

Latest Version Php Version Swoole Version Swoole License

Introduction

由Swoole驱动的Grpc协程客户端, 底层使用高性能协程Http2-Client客户端

  • 同步代码几乎无改动
    • 自动协程调度获得异步高性能
    • 提供Grpc代码生成器Plus版, 0成本迁移
  • 基于Channel实现的消息生产消费
    • 一个客户端连接即可同时hold住上万个请求响应
    • 支持跨协程并发, 多类型Client分享同一连接
  • Etcd的直接支持
    • 使用Http2协议全双工通信+Protobuf极限压缩, 告别同步阻塞与Json打包的低性能

Requirement

  • PHP7及以上
  • Swoole: v4.4.0及以上, StreamingCall支持需要v4.5.3及以上
  • Protobuf
  • grpc_php_plugin
  • 请不要启用grpc的php扩展, 也无需grpc的php库

Usage

仓库已提供Etcd的生成代码, 如要自己根据proto文件生成代码, 请使用tools目录下的生成工具generator, 使用方法和protoc命令完全一样, 增强了支持以目录作为参数, 自动查找目录下的proto文件生成, 如: 该目录下已提供的grpc生成代码脚本:

# it's generate_grpc.sh
./generator \
--proto_path=./../src/Grpc/Proto \
--php_out=./../src/Grpc \
--grpc_out=./../src/Grpc \
--plugin=protoc-gen-grpc=$1 \
./../src/Grpc/Proto

只需要将proto文件放在Grpc/Proto下, 运行./generate_grpc.sh ../../grpc/bins/opt/grpc_php_plugin (参数是你的grpc php插件位置, 一般在grpc/bins/opt目录中), 即可生成相关代码


Examples

以下示例都可在examples目录下找到并直接运行

Grpc


HelloWorld

经典的Grpc官方示例, 代码更加简洁

$greeterClient = new GreeterClient('127.0.0.1:50051');
$request = new HelloRequest();
$request->setName('Swoole');
list($reply, $status) = $greeterClient->SayHello($request);
$message = $reply->getMessage();
echo "{$message}\n"; // Output: Hello Swoole

Etcd


Etcd的几个基本操作的使用

Put

use Swoole\Coroutine;

Coroutine::create(function () {
    $kvClient = new Etcdserverpb\KVClient(GRPC_SERVER_DEFAULT_URI);
    $request = new Etcdserverpb\PutRequest();
    $request->setPrevKv(true);
    $request->setKey('Hello');
    $request->setValue('Swoole');
    [$reply, $status] = $kvClient->Put($request);
    if ($status === 0) {
        echo "{$reply->getPrevKv()->getKey()}\n";
        echo "{$reply->getPrevKv()->getValue()}\n";
    } else {
        echo "Error#{$status}: {$reply}\n";
    }
    $kvClient->close();
});

Watch

创建一个协程负责Watch, 创建两个协程定时写入/删除键值以便观察效果

use Etcdserverpb\WatchCreateRequest;
use Etcdserverpb\WatchCreateRequest\FilterType;
use Etcdserverpb\WatchRequest;
use Swoole\Coroutine;

// The Watcher
Coroutine::create(function () {
    $watchClient = new Etcdserverpb\WatchClient(GRPC_SERVER_DEFAULT_URI);

    $watchCall = $watchClient->Watch();
    $request = new WatchRequest();
    $createRequest = new WatchCreateRequest();
    $createRequest->setKey('Hello');
    $request->setCreateRequest($createRequest);

    _retry:
    $watchCall->push($request);
    /**@var $reply Etcdserverpb\WatchResponse */
    while (true) {
        [$reply, $status] = $watchCall->recv();
        if ($status === 0) { // success
            if ($reply->getCreated() || $reply->getCanceled()) {
                continue;
            }
            foreach ($reply->getEvents() as $event) {
                /**@var $event Mvccpb\Event */
                $type = $event->getType();
                $kv = $event->getKv();
                if (FilterType::NOPUT === $type) {
                    echo "Put key {$kv->getKey()} => {$kv->getValue()}\n";
                    break;
                } elseif (FilterType::NODELETE === $type) {
                    echo "Delete key {$kv->getKey()}\n";
                    break;
                }
            }
        } else { // failed
            static $retry_time = 0;
            if ($watchClient->isConnected()) {
                $retry_time++;
                echo "Retry#{$retry_time}\n";
                goto _retry;
            } else {
                echo "Error#{$status}: {$reply}\n";
                break;
            }
        }
    }
    $watchClient->close();
});

// The Writer Put and Delete
Coroutine::create(function () {
    $kvClient = new Etcdserverpb\KVClient(GRPC_SERVER_DEFAULT_URI);
    Coroutine::create(function () use ($kvClient) {
        $request = new Etcdserverpb\PutRequest();
        $request->setKey('Hello');
        $request->setPrevKv(true);
        while (true) {
            static $count = 0;
            Coroutine::sleep(.5);
            $request->setValue('Swoole#' . (++$count));
            [$reply, $status] = $kvClient->Put($request);
            if ($status !== 0) {
                echo "Error#{$status}: {$reply}\n";
                break;
            }
        }
        $kvClient->close();
    });
    Coroutine::create(function () use ($kvClient) {
        $request = new Etcdserverpb\DeleteRangeRequest();
        $request->setKey('Hello');
        $request->setPrevKv(true);
        while (true) {
            Coroutine::sleep(1);
            [$reply, $status] = $kvClient->DeleteRange($request);
            if ($status !== 0) {
                echo "Error#{$status}: {$reply}\n";
                break;
            }
        }
        $kvClient->close();
    });
});

Auth and Share Client

用户添加/展示/删除以及展示了如何让不同类型的EtcdClient能够使用同一个Grpc\Client创建的连接

use Swoole\Coroutine;

Coroutine::create(function () {
    $grpcClient = new Grpc\Client(GRPC_SERVER_DEFAULT_URI);
    // use in different type clients

    Coroutine::create(function () use ($grpcClient) {
        $kvClient = new Etcdserverpb\KVClient(GRPC_SERVER_DEFAULT_URI, ['use' => $grpcClient]);
        $request = new Etcdserverpb\PutRequest();
        $request->setPrevKv(true);
        $request->setKey('Hello');
        $request->setValue('Swoole');
        [$reply, $status] = $kvClient->Put($request);
        if ($status === 0) {
            echo "\n=== PUT KV OK ===\n";
        } else {
            echo "Error#{$status}: {$reply}\n";
        }
    });

    Coroutine::create(function () use ($grpcClient) {
        $authClient = new Etcdserverpb\AuthClient(GRPC_SERVER_DEFAULT_URI, ['use' => $grpcClient]);

        $userRequest = new Etcdserverpb\AuthUserAddRequest();
        $userNames = ['ranCoroutine::create', 'twosee', 'gxh', 'stone', 'sjl'];
        foreach ($userNames as $username) {
            $userRequest->setName($username);
            [$reply, $status] = $authClient->UserAdd($userRequest);
            if ($status !== 0) {
                goto _error;
            }
        }

        $useListRequest = new Etcdserverpb\AuthUserListRequest();
        [$reply, $status] = $authClient->UserList($useListRequest);
        if ($status !== 0) {
            goto _error;
        }
        echo "\n=== SHOW USER LIST ===\n";
        foreach ($reply->getUsers() as $user) {
            /**@var \Authpb\User */
            echo "* {$user}\n";
        }
        echo "=== SHOW USER LIST OK ===\n";

        $userRequest = new Etcdserverpb\AuthUserDeleteRequest();
        foreach ($userNames as $username) {
            $userRequest->setName($username);
            [$reply, $status] = $authClient->UserDelete($userRequest);
            if ($status !== 0) {
                goto _error;
            }
        }

        if (false) {
            _error:
            echo "Error#{$status}: {$reply}\n";
        }

        echo "\n=== SHOW ALL CLIENT STATS ===\n";
        var_dump(grpc_client_num_stats());
        $grpcClient->close();
    });

});

grpc's People

Contributors

cragonnyunt avatar huangzhhui avatar inhere avatar matyhtf avatar reasno avatar sy-records avatar twose avatar woody712 avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

grpc's Issues

grpc/greeter_server.php: [WARNING] swSSL_accept: bad SSL client

您好 @twose ,我正在实现 SSL 相关的功能,但是出现了问题完全阻塞了我的开发

  1. server.php:
$http = new swoole_http_server('127.0.0.1', 50051, SWOOLE_PROCESS, SWOOLE_SOCK_TCP | SWOOLE_SSL);
$http->set([
    'log_level' => SWOOLE_LOG_INFO,
    'trace_flags' => 0,
    'worker_num' => 1,
    'open_http2_protocol' => true,
    'ssl_cert_file' => './certs/server.pem',
    'ssl_key_file' => './certs/server-key.pem',
]);

$http->on('workerStart', function (swoole_http_server $server) {
    echo "nghttp -v https://127.0.0.1:{$server->port}\n";
});
$http->on('request', function (swoole_http_request $request, swoole_http_response $response) {
...
}
  1. client.php:
use \User\UserClient;
use \User\ListRequest;

require __DIR__ . '/../vendor/autoload.php';

$name = !empty($argv[1]) ? $argv[1] : 'Swoole User';

go(function () use ($name) {
    $credentials = Grpc\ChannelCredentials::createSsl(
        file_get_contents('../certs/ca.pem')
    );
    $userClient = new UserClient('127.0.0.1:50051', [
        'credentials' => $credentials,
    ]);
    $userClient->start();
    $request = new ListRequest();
    ...

    list($reply, $status) = $userClient->List($request);
    $message = $reply->getResponseName();
    echo "{$message}\n";
    $userClient->close();
});
  1. 场景回放:
    server.php 运行成功,client.php 执行后,server 端出现报错
$ php server.php
nghttp -v https://127.0.0.1:50051
[2018-10-20 15:15:27 #66954.0]	WARNING	swSSL_accept: bad SSL client[127.0.0.1:55810].
  1. 证书

我代码中的 ./certs/server.pem./certs/server-key.pem 在纯 Go Client/Server 中使用正常,PHP Client 是在官方 demo 中看到使用 ca.pem 即可

  1. 疑问:

Requirement 中有明确提到 不要启用grpc的php扩展

但是在 grpc-client 的包内并不包含 Grpc\ChannelCredentials 等等类,因此无法调用 SSL 方面的凭证处理,最后我被迫启动了 php grpc 的扩展来达到调用 ChannelCredentials 类的目的

但是又遇到了 bad SSL client 的问题,无法内调成功!

想请问 @twose 是否有建议或如何解决?或是我哪里做错了?

Client 复用同个连接,出现 rpc error: code = Unknown desc =

问题

客户端复用 ClientConn 后,无法再次调用 RPC 方法成功

server

直接使用 greeter_server.php

client

直接改造 greeter_client.go,如下:

package main

import (
	"log"
	"os"

	"golang.org/x/net/context"
	"google.golang.org/grpc"
	pb "google.golang.org/grpc/examples/helloworld/helloworld"
)

const (
	address     = "localhost:50051"
	defaultName = "world"
)

func main() {
	// Set up a connection to the server.
	conn, err := grpc.Dial(address, grpc.WithInsecure())
	if err != nil {
		log.Fatalf("did not connect: %v", err)
	}
	defer conn.Close()
	c := pb.NewGreeterClient(conn)

	// Contact the server and print out its response.
	name := defaultName
	if len(os.Args) > 1 {
		name = os.Args[1]
	}
	r, err := c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
	if err != nil {
		log.Fatalf("could not greet1: %v", err)
	}
	log.Printf("Greeting: %s", r.Message)

	r, err = c.SayHello(context.Background(), &pb.HelloRequest{Name: name})
	if err != nil {
		log.Fatalf("could not greet2: %v", err)
	}
	log.Printf("Greeting: %s", r.Message)
}

输出结果

$ go run greeter_client.go
2019/04/18 14:18:06 Greeting: Hello world
2019/04/18 14:18:06 could not greet2: rpc error: code = Unknown desc = 
exit status 1

关于 grpc/greeter_server.php 的一些疑问

@twose 又来打扰您了,我看了 grpc/greeter_server.php, 有了以下的疑问想要请教您 🤔

--

在示例代码中,是直接指定 SayHelloHelloRequestHelloReply 去响应的。而在真实的案例中,一个 server 端,是会承载多个 service,也就承载着对应的多个 rpc 接口

是需要判断对应是来自哪个 service,哪个 rpc 接口方法。像 Go 的 gRPC Server 的话,是会将其注册到内部的服务管理中,然后进行各种内部服务发现判断

那么这个 demo 显然就无法处理,想请问 twose 对此有没有什么好的实现方案或建议?

莫非是在 swoole_http_request $request 中能够取到对应的 service 和 rpc 方法接口名称,然后做映射吗?

$http->on('request', function (swoole_http_request $request, swoole_http_response $response) use ($http) {
    /**@var $request_message HelloRequest */
    $request_message = Grpc\Parser::deserializeMessage([HelloRequest::class, null], $request->rawcontent());
    if ($request_message) {
        $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));
    } else {
        $response->end('failed');
    }
});

感谢!

Not sure how to properly install

I'm trying to install this one, but I run into issues (probably due to mis-translation)

Anyhow, it seems to say that I should install Protobuf and grpc_php_plugin, but I should not enable these plugins? Does it mean I should not add the extensions to the conf.d folder of PHP?

Also, when I tried to run generate_grpc.sh I get the following error:

program not found or is not executable
--grpc_out: protoc-gen-grpc: Plugin failed with status code 1.

Please let me know what I should do to fix this.

并发量大的时候经常发生错误

显示:
Fatal error: Uncaught Swoole\Error: Socket#33 has already been bound to another coroutine#852, writing of the same socket in coroutine#853 at the same time is not allowed in

src目录的helloword能删除么

文件路径: src/Grpc/Helloworld

建议保持包的简洁性,如果使用该包作为客户端时,采用grpc-go包的helloworld.proto时会产生冲突

解析报错PHP Fatal error: Uncaught Google\Protobuf\Internal\GPBDecodeException: Error occurred during parsing: Unexpected wire type.

解析复杂grpc对象报错PHP Fatal error: Uncaught Google\Protobuf\Internal\GPBDecodeException: Error occurred during parsing: Unexpected wire type.
propto定义大概如下:

service UserService {
rpc GetUserList (UserListRequest) returns (UserListReply) {}
}
message User {
int64 id = 1; // 自增ID
int64 uid = 2; // UID
string user_name = 3; // 积分名称
}
message UserListRequest {
}
message UserListReply {
repeated User list = 1; //用户列表
}

GRPC断线重连问题

服务端意外断开链接后,客户端会卡死
跟踪代码,会以 client.php 里的 start 函数进入死循环,
看到swoole文档中说:
4.x 协程版本后,connected 属性不再会实时更新,isConnect 方法不再可靠

有什么解决办法吗

Swoole/grpc 无法调用node官方grpc-server

Swoole版本4.5.2

启动example/grpc/greater_client.php, 调用官方node grpc server,无响应,卡死。node server未进入响应逻辑。

@huanghantao

node

/*
 *
 * Copyright 2015 gRPC authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 */

var PROTO_PATH = __dirname + '/grpc/helloworld.proto';

var grpc = require('grpc');
var protoLoader = require('@grpc/proto-loader');
var packageDefinition = protoLoader.loadSync(
    PROTO_PATH,
    {keepCase: true,
        longs: String,
        enums: String,
        defaults: true,
        oneofs: true
    });
var hello_proto = grpc.loadPackageDefinition(packageDefinition).helloworld;

/**
 * Implements the SayHello RPC method.
 */
function sayHello(call, callback) {
    callback(null, {message: 'Hello Node Server - ' + call.request.name});
}

/**
 * Starts an RPC server that receives requests for the Greeter service at the
 * sample server port
 */
function main() {
    var server = new grpc.Server();
    server.addService(hello_proto.Greeter.service, {sayHello: sayHello});
    server.bind('0.0.0.0:9502', grpc.ServerCredentials.createInsecure());
    server.start();
}

main();

php

<?php

use Helloworld\GreeterClient;
use Helloworld\HelloRequest;

require __DIR__ . '/../../vendor/autoload.php';

$name = !empty($argv[1]) ? $argv[1] : 'Swoole';

Swoole\Coroutine::create(function () use ($name) {
    $greeterClient = new GreeterClient('127.0.0.1:9502');
    $request = new HelloRequest();
    $request->setName($name);
    [$reply] = $greeterClient->SayHello($request);
    $message = $reply->getMessage();
    echo "{$message}\n";
    $greeterClient->close();
});

php8.2兼容

在 openStream 函数中动态添加了$userPipelineRead属性导致php8.2不兼容

    public function openStream(string $path, $data = '', string $method = '', bool $use_pipeline_read = false): int
    {
        $request = new Request;
        if ($method) {
            $request->method = $method;
        } else {
            if (!$data) {
                $request->method = 'GET';
            } else {
                $request->method = 'POST';
            }
        }
        $request->path = $path;
        if ($data) {
            $request->data = $data;
        }
        $request->pipeline = true;
        if ($use_pipeline_read) {
            if (SWOOLE_VERSION_ID < 40503) {
                throw new InvalidArgumentException('Require Swoole version >= 4.5.3');
            }
            $request->usePipelineRead = true;
        }

        return $this->send($request);
    }

报错

Uncaught ErrorException: Creation of dynamic property Grpc\Request::$usePipelineRead is deprecated in /Users/nashgao/Desktop/project/space/dependencies/lib/space-utils/vendor/swoole/etcd-client/src/Grpc/Client.php:296

我这边不确定这个$usePipelineRead属性是放在\Grpc\Request中还是\Swoole\Http2\Request 希望可以兼容一下

PHP gRPC Client 调用 Swoole PHP Server 出现 Status Code 为 2

PHP 版本

$ php -v
PHP 7.2.11 (cli) (built: Oct 21 2018 18:28:44) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.2.0, Copyright (c) 1998-2018 Zend Technologies
    with Zend OPcache v7.2.11, Copyright (c) 1999-2018, by Zend Technologies

Swoole 版本

$ php --ri swoole

swoole

swoole support => enabled
Version => 4.2.3
Author => Swoole Group[email: [email protected]]
coroutine => enabled
kqueue => enabled
rwlock => enabled
sockets => enabled
openssl => OpenSSL 1.0.2p  14 Aug 2018
http2 => 1.34.0
pcre => enabled
zlib => enabled
brotli => enabled

Directive => Local Value => Master Value
swoole.enable_coroutine => On => On
swoole.aio_thread_num => 2 => 2
swoole.display_errors => On => On
swoole.use_namespace => On => On
swoole.use_shortname => On => On
swoole.fast_serialize => Off => Off
swoole.unixsock_buffer_size => 8388608 => 8388608

背景

1、Server 端:基于 Swoole 的 swoole_http_server 搭建的 gRPC Server
2、Client 端:原生 gRPC 官方提供的 PHP Client

场景

在调用 PHP Client 请求 PHP Server 时,在初次请求中。第一个调用是正常 status code,但是第二个调用就返回 status code 为 2。但是接下来多次请求均正常反馈(不重启 server 的情况下,重启的话又会出现)

返回

第一个调用

stdClass Object
(
    [metadata] => Array
        (
        )

    [code] => 0
    [details] => 
)

$reply 和 $status 均为正常值

第二个调用

stdClass Object
(
    [metadata] => Array
        (
            [server] => Array
                (
                    [0] => swoole-http-server
                )

            [content-encoding] => Array
                (
                    [0] => gzip
                )

        )

    [code] => 2
    [details] => 
)

$reply 为正常值,$status->code 为 2,存在问题!

问题

我的 2个 Client 调用,两个分别单独调用都是正常的。但是一旦都打开,就会出现问题。并且出现问题的那个响应结果 metadata,一定会出现:

 [server] => Array
(
    [0] => swoole-http-server
)
...

只有在初次调用下,存在多个 Client 请求,才会出现这个问题!

第二次一模一样的代码调用就又正常了。考虑到表现上出现了 swoole 的标识符。怀疑是否 swoole 内部做了什么初始化动作,导致 gRPC Status Code 出现异常。但接下来又正常了....

It doesn't work in swoole v4.3.2

I used this client in v4.2.9, everything is OK.

And I updated swoole to v4.3.2, the client can not work.

`
$kvClient = new \Etcdserverpb\KVClient($host);

$kvClient->start();

$rangeRequest = new \Etcdserverpb\RangeRequest();

$rangeRequest->setKey('key');

$rangeRequest->setRangeEnd($this->addOneBit('key'));

$results = $kvClient->Range($rangeRequest);

$rangeResponse = $results[0];
`
there is no respone.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.