GRPC 服务调用实践(一)

服务化

  • 最近在看微服务相关的设计,其中 grpc 算是微服务框架的标配所以要研究一下
  • 本文适合对 grpc 有一点印象的,不知道的站内可以搜下,golang 板块有人写的挺好的,我就不写了~
  • 因为公司有使用 三种语言的团队,所以寻求一种除了,http 之外更高效的协议,才有了之后的事情,最后挺一下 swofthyperf 加油!

gin 实现 grpc 简单的应答

安装protobuf

1、安装相关软件 ,我用的是 contOS7+windows , mac应该更好装一点

    yum install autoconf automake libtool gcc gcc-c++ zlib-devel

2、 下载protobuf,并安装

去到 Protocol Buffers 下载最新版本,然后解压到本地。

    wget https://github.com/protocolbuffers/protobuf/releases/download/v3.10.1/protoc-3.10.1-linux-x86_64.zip
    unzip protoc-3.10.1-linux-x86_64.zip
    protoc --version # 能看到 版本信息就安装成功了

安装 protobuf golang 插件

    go get -u github.com/golang/protobuf/{proto,protoc-gen-go}

前提条件

  • go 1.13 以上版本

  • go.mod 代理 ok

          export GOPROXY=https://goproxy.cn
          export GO111MODULE=on
  • 目录如下

      - rpc-hello
          - pb
              - hello.proto
          - go.mod
  • hello.proto 内容如下

      syntax = "proto3";
    
      package hello;
    
      // 定义服务
      service Hello {
          rpc SayHello (HelloRequest) returns (HelloReply) {}
      }
    
      // 请求体的结构体
      message HelloRequest {
    
      string name = 1;
    
      }
    
      // 响应的结构体
      message HelloReply {
          string message = 1;
          int64 code = 2;
      }
    
  • 进入 pb 下 执行

    protoc --go_out=plugins=grpc:. hello.proto
  • 会发现 多了rpc-hello\pb\hello.pb.go 这样的一个文件

grpc四种服务类型:

1、简单方式:这就是一般的rpc调用,一个请求对象对应一个返回对象

2、服务端流式(Sever-side streaming )

3、客户端流式(Client-side streaming RPC)

4、双向流式(Bidirectional streaming RPC)

简单方式的调用

  • 更改目录为

      rpc-hello
          - pb
              - hello.proto
              - hello.pb.go
          - go.mod
          - service
              - service.go
          - client
              - client.go
  • 服务端 实现 service

    
      package main
    
      import (
          "golang.org/x/net/context"
          "google.golang.org/grpc"
          "google.golang.org/grpc/reflection"
          "log"
          "net"
          pb "rpc-hello/pb"
      )
    
      // server 用来实现 hello.HelloServer
    
      type server struct{}
    
      // 实现 hello.SayHello 方法
    
      // (context.Context, *HelloRequest) (*HelloReply, error)
    
      func (s *server) SayHello(ctx context.Context, in *pb.HelloRequest) (*pb.HelloReply, error) {
          return &pb.HelloReply{Message: "Hello " + in.Name, Code: 200}, nil
      }
    
      func main() {
    
          lis, err := net.Listen("tcp", ":50051")
    
          if err != nil {
              log.Fatalf("failed to listen: %v", err)
          }
    
          s := grpc.NewServer()
    
          pb.RegisterHelloServer(s, &server{})
    
          //在 server 中 注册 gRPC 的 reflection service
    
          reflection.Register(s)
    
          if err := s.Serve(lis); err != nil {
              log.Fatalf("failed to serve: %v", err)
          }
      }
    
  • 客户端 实现 client.go

      package main
    
      import (
          "fmt"
          "github.com/gin-gonic/gin"
          "google.golang.org/grpc"
          "log"
          "net/http"
          pb "rpc-hello/pb"
      )
    
      func main() {
    
          r := gin.Default()
    
          r.GET("/rpc/hello", func(c *gin.Context) {
              sayHello(c)
          })
    
          // Run http server
          if err := r.Run(":8052"); err != nil {
              log.Fatalf("could not run server: %v", err)
          }
      }
    
      func sayHello(c *gin.Context) {
    
          // Set up a connection to the server.
          conn, err := grpc.Dial("localhost:50051", grpc.WithInsecure())
          if err != nil {
              log.Fatalf("did not connect: %v", err)
          }
    
          defer conn.Close()
    
          client := pb.NewHelloClient(conn)
          name := c.DefaultQuery("name","战士上战场")
          req := &pb.HelloRequest{Name: name}
          res, err := client.SayHello(c, req)
    
          if err != nil {
              c.JSON(http.StatusInternalServerError, gin.H{
                  "error": err.Error(),
              })
              return
          }
    
          c.JSON(http.StatusOK, gin.H{
              "result": fmt.Sprint(res.Message),
              "code": fmt.Sprint(res.Code),
          })
    
      }
  • 先 执行 go run service.go 切换另一个终端执行 go run client.go 访问 http://xxx.xxx.xx.xx:8052/rpc/hello 就能看到效果~

php 原生实现客户端

安装 protoc-gen-php 扩展

git clone -b $(curl -L https://grpc.io/release) https://github.com/grpc/grpc
cd grpc
git submodule update --init # 这一步 要下4个小时,建议还是要挂git代理
make grpc_php_plugin # 警告没事,没error 就行
# 建议 国内用户换这个 ,再切换到对应版本分支,不然太难受了
git clone https://gitee.com/devin2019/grpc.git
cd grpc
git checkout xxxx # 对应分支
git submodule update --init #这一步 要下4个小时,建议还是要挂git代理
make grpc_php_plugin # 警告没事,没error 就行
  • 最终 grpc_php_plugin 在 你的 /pathto/grpc/bins/opt/grpc_php_plugin

安装 grpc 扩展

wget https://pecl.php.net/get/grpc-1.25.0.tgz
tar -zxvf grpc-1.25.0.tgz
/usr/bin/phpize #(这个根据`phpize`实际情况来)
./configure --with-php-config=/usr/bin/php-config #(这个根据`php-config`实际情况来)
make && make install
vim /etc/php.d/grpc.ini #这个根据实际情况去决定 是改`php.ini`还是别的什么
写入 extension=grpc.so

安装 protobuf 扩展

  • 要想 gRPC获得更好的性能,就安装 protobuf 扩展

      wget https://pecl.php.net/get/protobuf-3.10.0.tgz
      tar -zxvf protobuf-3.10.0.tgz
      /usr/bin/phpize #(这个根据`phpize`实际情况来)
      ./configure --with-php-config=/usr/bin/php-config #(这个根据`php-config`实际情况来)
      make && make install
      vim /etc/php.d/protobuf.ini #这个根据实际情况去决定 是改`php.ini`还是别的什么
      写入 extension=protobuf.so
  • 偷懒做法 就直接 composer 拉个包

      "require": {
          "google/protobuf": "^v3.10.0"
      },

生成文件 带客户端

  • 使用命令为 protoc --php_out=./ --grpc_out=./ --plugin=protoc-gen-grpc=/root/go/bin/grpc_php_plugin hello.proto 这个是带 Client的做法

  • 这里 我 把 grpc_php_plugin 做了软链接 你按安装时的位置输出就好

  • 不带 Client 的 为 protoc --php_out=plugins=grpc:. grpc.proto

      |-- composer.json
      |-- composer.lock
      |-- GPBMetadata
          | |-- Hello.php
      |-- Hello
          | |-- HelloClient.php
          | |-- HelloReply.php
          | |-- HelloRequest.php
      |-- hello.proto
      |-- index.php
      |-- vendor

index.php

<?php

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

use \Grpc\ChannelCredentials;
use \Hello\HelloClient;
use \Hello\HelloRequest;
use \Hello\HelloReply;

// 创建客户端实例

$helloClient = new HelloClient('127.0.0.1:50051', [
    'credentials' => ChannelCredentials::createInsecure()
]);

$helloRequest = new HelloRequest();
$helloRequest->setName("有事别找我");
$request = $helloClient->SayHello($helloRequest)->wait();

//返回数组

/** @var array $status */
/** @var HelloReply $response */
list($response, $status) = $request;

var_dump($response->getMessage());
echo PHP_EOL;
var_dump($status);

composer.json

{
    "name": "rpc/test",
    "require": {
        "grpc/grpc": "v1.25.0",
        "google/protobuf": "^v3.10.0"
    },
    "autoload":{
        "psr-4":{
            "GPBMetadata\\":"GPBMetadata/",
            "Hello\\":"Hello/"
        }
    },
    "repositories": {
        "packagist": {
            "type": "composer",
            "url": "https://mirrors.aliyun.com/composer/"
        }
    }
}
  • 先做了缓存的,一定要在改过之后 composer dump -o

  • 开启 service.go 运行 php index.php 客户端就完成了

php swoole 框架 hyperf 实现 实现客户端 与服务端 与 golang 服务互调

  • 官方文档
  • swoole 安装就不讲了,比起 swoole2.x 现在的安装也很简单了
  • hyperf 安装也很简单,唯一要 注意的是 需要 在 php.ini 中 加入 swoole.use_shortname=off
  • 使用 composer create-project hyperf/hyperf-skeleton 来 构建基础代码
  • 选中 grpc 服务其它的额外组件都不用选

服务端

  • 我们先将 上一步 原生生成的文件 ,在项目根目录下加一个 grpc 目录,内部结构如下

      |-- GPBMetadata
          | |-- Hello.php
      |-- Hello
          | |-- HelloClient.php
          | |-- HelloReply.php
          | |-- HelloRequest.php
  • 更改 composer.json 同样是在 psr-4 中 加入

          "GPBMetadata\\": "grpc/GPBMetadata",
          "Grpc\\": "grpc/Grpc"
  • config/autoload/server 中 加入

     [
         'name' => 'grpc',
         'type' => Server::SERVER_HTTP,
         'host' => '0.0.0.0',
         'port' => 50051,
         'sock_type' => SWOOLE_SOCK_TCP,
         'callbacks' => [
                 SwooleEvent::ON_REQUEST => [\Hyperf\GrpcServer\Server::class, 'onRequest'
             ],
         ],
     ],
  • config/routes.php里加入

      Router::addServer("grpc", function () {
          Router::addGroup('/hello.Hello', function () {
              Router::post('/SayHello', 'App\Controller\IndexController@sayHello');
          });
      });
  • app/Controller/IndexController.php 中 加入

      public function sayHello(HelloRequest $request)
      {
          $message = new HelloReply();
          $message->setMessage("Hello {$request->getName()}");
          $message->setCode(200);
          return $message;
      }
    
  • 这里要注意的是 .proto 文件中的定义和 gRPC server 路由的对应关系:/{package}.{service}/{rpc}

  • 服务端封装详情可以看源码,也不难

客户端

  • app/Controller/IndexController.php 中修改

      use Hello\HelloClient;
      use Hello\HelloReply;
      use Hello\HelloRequest;
      use Hyperf\HttpServer\Contract\RequestInterface;
    
      public function index(RequestInterface $request)
      {
          $client = new HelloClient("127.0.0.1:50051", ['credentials' => null]);
    
          $name = $request->input("name","战士上战场");
          $helloRequest = new HelloRequest();
          $helloRequest->setName($name);
    
          /**
          * @var HelloReply $reply
          */
          list($reply, $status) = $client->sayHello($helloRequest);
    
          $message = $reply->getMessage();
          $code = $reply->getCode();
    
          $client->close();
          var_dump(memory_get_usage(true));
    
          return [
              'message' => $message,
              'code' => $code,
          ];
      }
  • 上一步中 用了 HelloClient 看看 框架封装,跟原生的区别

  • 封装的

    
      namespace Hello;
    
      use Hyperf\GrpcClient\BaseClient;
    
      /**
      * 定义服务
      */
      class HelloClient extends BaseClient{
    
          public function sayHello(HelloRequest $argument)
          {
              return $this->simpleRequest(
                      '/hello.Hello/SayHello',
                      $argument,
                      [HelloReply::class, 'decode']
                  );
          }
      }
  • 自动生成的

      namespace Hello;
    
      /**
      * 定义服务
      */
      class HelloClient extends \Grpc\BaseStub {
    
          /**
          * @param string $hostname hostname
          * @param array $opts channel options
          * @param \Grpc\Channel $channel (optional) re-use channel object
          */
          public function __construct($hostname, $opts, $channel = null) {
              parent::__construct($hostname, $opts, $channel);
          }
    
          /**
          * @param \Hello\HelloRequest $argument input argument
          * @param array $metadata metadata
          * @param array $options call options
          */
    
          public function SayHello(\Hello\HelloRequest $argument,
          $metadata = [], $options = []) {
              return $this->_simpleRequest(
                                  '/hello.Hello/SayHello',
                                  $argument,
                                  ['\Hello\HelloReply', 'decode'],
                                  $metadata, $options);
              }
    
      }
  • 结论是其实差不了多少

  • 然后 你可以试试 php grpc 服务端 跟 golang grpc 客户端的互调

  • 果然装环境才是学习程序最难的地方,特别是这个下了4个小时的git命令,太南了0.0

本作品采用《CC 协议》,转载必须注明作者和本文链接
本帖由系统于 2年前 自动加精
讨论数量: 9

修改grpc里gitmodule的仓库地址
我用的gitee的仓库
也可以不用等4小时 而且要修改makefile里的

272AR = ar -r
4年前 评论

有没有现成的grpc_php_plugin,哪里能下载的

4年前 评论
mdx86 3年前

grpc的grpc_python_plugin生成的python客户端可以调用PHP服务端吧?

4年前 评论

2020-07-08 11:11:10 更新:
hyperf 不能使用 protoc-gen-grpc 生成的客户端

github.com/hyperf/hyperf/issues/20...

我在使用自动生成的 client 时会卡在$this->_simpleRequest(...)这个方法里面不会返回结果。
具体是走到vendor/grpc/grpc/src/lib/BaseStub.php_GrpcUnaryUnary方法中的$call->start($argument, $metadata, $options); 就没有返回了,一直处于等待中。
但我换成 laravel 框架之后就不存在这个问题。
所以楼主跑过自动生成的 client 客户端时没有遇到过这个问题吗?我在想会不会是常驻内存的特性导致的这个问题。

3年前 评论

大佬你好,求一份 hyperf grpc 的 demo

3年前 评论

大佬 ,有没有php的服务端用ab压测过

2年前 评论

怎么用php 实现grpc流双向传输 实时推送 实时获取数据

1个月前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!