gRPC雙向資料流的互動控制(go語言實現)| gRPC雙向資料流的互動控制系列(1)

阿狸不歌發表於2018-10-13

gRPC簡介

gRPC (https://grpc.io) 是一個由Google開發的高效能、開源、跨多種程式語言和通用的遠端過程呼叫協議(RPC) 框架,用於客戶端和伺服器端之間的通訊,使用HTTP/2協議並將 ProtoBuf (https://developers.google.com/protocol-buffers)作為序列化工具。


gRPC模式

gRPC主要有4種請求/響應模式,分別是:

(1) 簡單模式(Simple RPC)

這種模式最為傳統,即客戶端發起一次請求,服務端響應一個資料,這和大家平時熟悉的RPC沒有什麼大的區別,所以不再詳細介紹。

(2) 服務端資料流模式(Server-side streaming RPC)

這種模式是客戶端發起一次請求,服務端返回一段連續的資料流。典型的例子是客戶端向服務端傳送一個股票程式碼,服務端就把該股票的實時資料來源源不斷的返回給客戶端。

(3) 客戶端資料流模式(Client-side streaming RPC)

與服務端資料流模式相反,這次是客戶端源源不斷的向服務端傳送資料流,而在傳送結束後,由服務端返回一個響應。典型的例子是物聯網終端向伺服器報送資料。

(4) 雙向資料流模式(Bidirectional streaming RPC)

顧名思義,這是客戶端和服務端都可以向對方傳送資料流,這個時候雙方的資料可以同時互相傳送,也就是可以實現實時互動。典型的例子是聊天機器人。


雙向資料流實戰

在gRPC中文文件(http://doc.oschina.net/grpc?t=60133)中有上述4種模式的例項,但是其中雙向資料流的例子過於簡單,沒有體現出雙向控制的特點,所以本文建立一個新的例子(用golang實現),用以展示gRPC雙向資料流的互動(關於proto如何定義、相關包如何安裝,在文件中都有介紹,所以本文略去此部分)。

1、proto定義

syntax = "proto3"; // 語法使用 protocol buffer proto3

// 包名: chat
package chat;   

/*
    服務名: Chat,
    其中只有 名為“BidStream”的一個RPC服務,
    輸入是 Request格式的資料流, 輸出是 Response 格式的資料流
*/
service Chat {    
    rpc BidStream(stream Request) returns (stream Response) {}
}

// 請求資料 Request格式定義
message Request {
    string input = 1;
}

// 響應資料Response格式定義
message Response {
    string output = 1;
}

服務端程式 server.go

package main

import (
    "io"
    "log"
    "net"
    "strconv"
    "google.golang.org/grpc"
    proto "chat" // 自動生成的 proto程式碼
)

// Streamer 服務端
type Streamer struct{}

// BidStream 實現了 ChatServer 介面中定義的 BidStream 方法
func (s *Streamer) BidStream(stream proto.Chat_BidStreamServer) error {
    ctx := stream.Context()
    for {
        select {
        case <-ctx.Done():
            log.Println("收到客戶端通過context發出的終止訊號")
            return ctx.Err()
        default:
            // 接收從客戶端發來的訊息
            輸入, err := stream.Recv()
            if err == io.EOF {
                log.Println("客戶端傳送的資料流結束")
                return nil
            }
            if err != nil {
                log.Println("接收資料出錯:", err)
                return err
            }

            // 如果接收正常,則根據接收到的 字串 執行相應的指令
            switch 輸入.Input {
            case "結束對話\n":
                log.Println("收到'結束對話'指令")
                if err := stream.Send(&proto.Response{Output: "收到結束指令"}); err != nil {
                    return err
                }
                // 收到結束指令時,通過 return nil 終止雙向資料流
                return nil

            case "返回資料流\n":
                log.Println("收到'返回資料流'指令")
                // 收到 收到'返回資料流'指令, 連續返回 10 條資料
                for i := 0; i < 10; i++ {
                    if err := stream.Send(&proto.Response{Output: "資料流 #" + strconv.Itoa(i)}); err != nil {
                        return err
                    }
                }

            default:
                // 預設情況下, 返回 '服務端返回: ' + 輸入資訊
                log.Printf("[收到訊息]: %s", 輸入.Input)
                if err := stream.Send(&proto.Response{Output: "服務端返回: " + 輸入.Input}); err != nil {
                    return err
                }
            }
        }
    }
}

func main() {
    log.Println("啟動服務端...")
    server := grpc.NewServer()

    // 註冊 ChatServer
    proto.RegisterChatServer(server, &Streamer{})

    address, err := net.Listen("tcp", ":3000")
    if err != nil {
        panic(err)
    }

    if err := server.Serve(address); err != nil {
        panic(err)
    }
}

客戶端程式 client.go

package main

import (
    "bufio"
    "context"
    "io"
    "log"
    "os"

    "google.golang.org/grpc"
    proto "chat" // 根據proto檔案自動生成的程式碼
)

func main() {
    // 建立連線
    conn, err := grpc.Dial("localhost:3000", grpc.WithInsecure())
    if err != nil {
        log.Printf("連線失敗: [%v]\n", err)
        return
    }
    defer conn.Close()

    // 宣告客戶端
    client := proto.NewChatClient(conn)

    // 宣告 context
    ctx := context.Background()

    // 建立雙向資料流
    stream, err := client.BidStream(ctx)
    if err != nil {
        log.Printf("建立資料流失敗: [%v]\n", err)
    }

    // 啟動一個 goroutine 接收命令列輸入的指令
    go func() {
        log.Println("請輸入訊息...")
        輸入 := bufio.NewReader(os.Stdin)
        for {
            // 獲取 命令列輸入的字串, 以回車 \n 作為結束標誌
            命令列輸入的字串, _ := 輸入.ReadString('\n')

            // 向服務端傳送 指令
            if err := stream.Send(&proto.Request{Input: 命令列輸入的字串}); err != nil {
                return
            }
        }
    }()

    for {
        // 接收從 服務端返回的資料流
        響應, err := stream.Recv()
        if err == io.EOF {
            log.Println("⚠️ 收到服務端的結束訊號")
            break    //如果收到結束訊號,則退出“接收迴圈”,結束客戶端程式
        }

        if err != nil {
            // TODO: 處理接收錯誤
            log.Println("接收資料出錯:", err)
        }

        // 沒有錯誤的情況下,列印來自服務端的訊息
        log.Printf("[客戶端收到]: %s", 響應.Output)
    }
}

執行效果

先啟動服務端程式 server.go 再啟動客戶端程式 client.go

輸入訊息,結果類似下圖:

執行截圖

總結

gRPC是個很強大的RPC框架,而且支援多語言程式設計,上面的服務端、客戶端程式我們完全可以用不同的語言實現,比如服務端用JAVA,客戶端用Python...

gRPC的四種互動模式也給我們提供了很大的發揮空間,最近Nginx宣佈支援gRPC,這可能也預示著某種趨勢... grpc + nginx


gRPC雙向資料流的互動控制系列

(之二): 通過Websocket與gRPC互動

(之三): 通過Nginx實現gRPC服務的負載均衡

相關文章