GRPC 服務呼叫實踐(一)

漫天風雨下西樓發表於2019-11-12
  • 最近在看微服務相關的設計,其中 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

相關文章