golang使用gRPC創建雙向流模式

gRPC庫介紹

gRPC是一個高性能、通用的開源RPC框架,其由Google主要面向移動應用開發並基於HTTP/2協議標準而設計,基於ProtoBuf(Protocol Buffers)序列化協議開發,且支持衆多開發語言。 gRPC提供了一種簡單的方法來精確地定義服務和爲iOS、Android和後臺支持服務自動生成可靠性很強的客戶端功能庫。 客戶端充分利用高級流和鏈接功能,從而有助於節省帶寬、降低的TCP鏈接次數、節省CPU使用、和電池壽命。
gRPC具有以下重要特徵:
1. 強大的IDL特性 RPC使用ProtoBuf來定義服務,ProtoBuf是由Google開發的一種數據序列化協議,性能出衆,得到了廣泛的應用。
2. 支持多種語言 支持C++、Java、Go、Python、Ruby、C#、Node.js、Android Java、Objective-C、PHP等編程語言。
3. 基於HTTP/2標準設計

gRPC安裝

grpc支持1.5及以上版本。
用以下命令安裝grpc-go:
go get google.golang.org/grpc
安裝Protocol Buffers v3
https://github.com/google/protobuf/releases下載最新的穩定的版本然後解壓縮,把裏面的文件放到’PATH’中。
安裝插件
go get -u github.com/golang/protobuf/{proto,protoc-gen-go}
別忘了將GOPATH/bin PATH中:
export PATH=PATH: GOPATH/bin

定義消息類型

message UserInfoResponse {
    string name     = 1; // 用戶姓名
    uint32 age      = 2; // 用戶年齡
    uint32 sex      = 3; // 用戶性別
    uint32 count    = 4; // 賬戶餘額
}

如上例子每個字段每個字段都有唯一的一個數字標識符,這些標識符是用來在消息的二進制格式中識別各個字段的,一旦開始使用就不能夠再改變。注:[1,15]之內的標識號在編碼的時候會佔用一個字節。[16,2047]之內的標識號則佔用2個字節。所以應該爲那些頻繁出現的消息元素保留 [1,15]之內的標識號。切記:要爲將來有可能添加的、頻繁出現的標識號預留一些標識號。
最小的標識號可以從1開始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的標識號, Protobuf協議實現中對這些進行了預留。如果非要在.proto文件中使用這些預留標識號,編譯時就會報警。

值類型

.proto Type C++ Java Python Go
double double double float float64
float float float float float32
int32 int32 int int int
int64 int64 long int/long[3] int64
uint32 uint32 int[1] int/long[3] uint32
uint64 uint64 long[1] int/long[3] uint64

官方示例代碼

示例代碼獲取地址:https://github.com/andyidea/go-example

定義服務

使用gRPC

  1. 在一個後綴名爲.proto的文件內定義服務。
  2. 用protocol buffer編輯器生成服務端和客戶端代碼。
  3. 使用gRPC的Go API實現客戶端與服務端代碼。

定義服務

要定義一個服務必須要在你的.proto文件中指定service,然後在你的服務中定義rpc方法,指定請求和響應類型。gRPC可定義4種類型的service方法。
1. 簡單的RPC,客戶端使用存根發送請求到服務器並等待響應返回,就像平常的函數調用。

rpc GetFeature(Point) returns (Feature) {}

2 . 一個服務端流式PRC,客戶端發送請求到服務器,拿到一個流去讀取返回的消息序列。客戶端讀取返回的流,知道里面沒有任何消息。從例子中我們可以看出,通過在響應類型前插入stream關鍵字就可以指定一個服務器端的流方法。

rpc ListFeatures(Rectangle) returns (stream Feature) {}

3 . 一個 客戶端流式 RPC , 客戶端寫入一個消息序列並將其發送到服務器,同樣也是使用流。一旦客戶端完成寫入消息,它等待服務器完成讀取返回它的響應。通過在 請求 類型前指定 stream 關鍵字來指定一個客戶端的流方法。

rpc RecordRoute(stream Point) returns (RouteSummary) {}

4 . 一個 雙向流式 RPC 是雙方使用讀寫流去發送一個消息序列。兩個流獨立操作,因此客戶端和服務器可以以任意喜歡的順序讀寫:比如, 服務器可以在寫入響應前等待接收所有的客戶端消息,或者可以交替的讀取和寫入消息,或者其他讀寫的組合。 每個流中的消息順序被預留。你可以通過在請求和響應前加 stream 關鍵字去制定方法的類型。

 rpc RouteChat(stream RouteNote) returns (stream RouteNote) {}

示例

gRPC介紹的差不多了,下面就動手開始寫一個示例,示例中使用了簡單的RPC以及雙向流式 RPC。

定義服務

首先我們要定義自己的後綴名爲.proto的文件,我的名字爲friday.proto

syntax = "proto3";

package friday;

// 請求用戶信息
message UserInfoRequest {
    int64 uid = 1; // 用戶ID
}

// 請求用戶信息的結果
message UserInfoResponse {
    string name     = 1; // 用戶姓名
    uint32 age      = 2; // 用戶年齡
    uint32 sex      = 3; // 用戶性別
    uint32 count    = 4; // 賬戶餘額
}

service Data {
    //簡單Rpc
    // 獲取用戶數據
    rpc GetUserInfo(UserInfoRequest) returns (UserInfoResponse){}

    //  修改用戶 雙向流模式
    rpc ChangeUserInfo(stream UserInfoResponse) returns (stream UserInfoResponse){}

}

定義完成後生成服務端與客戶端代碼

protoc --go_out=plugins=grpc:. friday.proto

完成後會在當前的文件夾中生成friday.pb.go的文件,打開文件我們就可以看到該文件中定義了客戶端與服務端的方法,這裏就不詳細的說明了,下面我們就開始動手實現。

編寫服務端代碼

package main

import (
    "net"
    "google.golang.org/grpc"
    pb "rpcTest/rpcbuild/rpcbuild/friday"
    "rpcTest/rpcbuild/response"
    "log"

)

const (
    PORT = ":10023"
)

func main() {
    lis, err := net.Listen("tcp", PORT)
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }
    s := grpc.NewServer()
    pb.RegisterDataServer(s, &response.Server{})
    s.Serve(lis)

}

爲了方便以後的擴展,在該文件中只是開啓了服務並監聽相關端口,下面是具體實現。由於是就簡單的demo服務端只是接收了請求並返回並沒有進行過多的操作,具體請查看代碼。

/*服務端的方法*/
package response

import (
    "golang.org/x/net/context"
    pb "rpcTest/rpcbuild/rpcbuild/friday"
    "fmt"
    "io"
)

type Server struct{
    routeNotes    []*pb.UserInfoResponse
}

//簡單模式
func (this *Server)GetUserInfo(ctx context.Context, in *pb.UserInfoRequest)(*pb.UserInfoResponse,error){
    uid := in.GetUid()
    fmt.Println("The uid is ",uid)
    return &pb.UserInfoResponse{
        Name : "Jim",
        Age  : 18,
        Sex : 0,
        Count:1000,
    },nil
}

//雙向流模式
func (this *Server) ChangeUserInfo(stream pb.Data_ChangeUserInfoServer)(error){
    for {
        in, err := stream.Recv()
        if err == io.EOF {
            fmt.Println("read done")
            return nil
        }
        if err != nil {
            fmt.Println("ERR",err)
            return err
        }
        fmt.Println("userinfo ",in)
        for _, note := range this.routeNotes{
            if err := stream.Send(note); err != nil {
                return err
            }
        }
    }
}

編寫客戶端代碼

客戶端使用了一個開源的通用的鏈接池,該鏈接池地址如下:
https://github.com/silenceper/pool
由於gRPC使用的是HTTP/2協議協議支持多路複用,在單個連接上實現同時進行多個業務單元數據的傳輸。故在客戶端加入該鏈接池。

package main

import (
    "google.golang.org/grpc"
    pb "rpcTest/rpcbuild/rpcbuild/friday"
    "rpcTest/rpcbuild/connect"
    "fmt"
    "sync"
)

const (
    address = "127.0.0.1:10023"
)

func main(){
    conn, err := grpc.Dial(address, grpc.WithInsecure())
    if err != nil {
        fmt.Println("did not connect: %v", err)
    }
    defer conn.Close()

    // 創建連接
    factory := func() (interface{}, error) {
        return pb.NewDataClient(conn),nil
    }
    // 關閉鏈接,此處只是定義不需要調用了因爲上面有defer conn.Close(),定義的目的在於初始化鏈接池。
    close := func(v interface{}) error { return conn.Close()}

    //初始化鏈接池
    p,err := connect.InitThread(10,30,factory)
    if err != nil{
        fmt.Println("init error")
        return
    }

    var wg sync.WaitGroup
    for i := 0;i < 50;i++ {
        wg.Add(1)
        go func(){
            defer wg.Done()
            //獲取連接
            v,_ := p.Get()
            client := v.(pb.DataClient)
            info := &pb.UserInfoRequest{
                Uid:10012,
            }
            connect.GetUserInfo(client,info)
            //歸還鏈接
            p.Put(v)
        }()
        wg.Wait()
    }

    for i := 0;i < 50;i++ {
        wg.Add(1)
        go func(){
            defer wg.Done()
            //獲取連接
            v,_ := p.Get()
            client := v.(pb.DataClient)
            connect.ChangeUserInfo(client)
            //歸還鏈接
            p.Put(v)
        }()
        wg.Wait()
    }
    //獲取鏈接池大小
    current := p.Len()
    fmt.Println("len=", current)
}

客戶端代碼,主要創建鏈接,初始化鏈接池,每次請求時都會先獲取一個鏈接用完之後歸還,爲了方便之後的擴展同樣將客戶端的拆分。

package connect

import (
    "github.com/silenceper/pool"
    "fmt"
    "time"
    "net"
)

/*
    初始化
    min // 最小鏈接數
    max // 最大鏈接數
    factory func() (interface{}, error) //創建鏈接的方法
    close func(v interface{}) error //關閉鏈接的方法
*/
func InitThread(min,max int,factory func() (interface{}, error),close func(v interface{}) error)(pool.Pool,error){

    poolConfig := &pool.PoolConfig{
        InitialCap: min,
        MaxCap:     max,
        Factory:    factory,
        Close:      close,
        //鏈接最大空閒時間,超過該時間的鏈接 將會關閉,可避免空閒時鏈接EOF,自動失效的問題
        IdleTimeout: 15 * time.Second,
    }
    p, err := pool.NewChannelPool(poolConfig)
    if err != nil {
        fmt.Println("Init err=", err)
        return nil,err
    }
    return p,nil
}

以上爲初始化鏈接池。

/*客戶端方法*/
package connect

import (
    "golang.org/x/net/context"
    pb "rpcTest/rpcbuild/rpcbuild/friday"
    "fmt"
    "io"
)

//簡單模式
func GetUserInfo(client pb.DataClient, info *pb.UserInfoRequest)  {
    req, err := client.GetUserInfo(context.Background(),info)
    if err != nil {
        fmt.Println("Could not create Customer: %v", err)
    }
    fmt.Println("userinfo is ",req.GetAge(),req.GetCount(),req.GetName(),req.GetSex())
}

//雙向流模式
func ChangeUserInfo(client pb.DataClient){
    notes := []*pb.UserInfoResponse{
        {Name:"jim",Age:18,Sex:2,Count:100},
        {Name:"Tom",Age:20,Sex:1,Count:666},
    }
    stream, err := client.ChangeUserInfo(context.Background())
    if err != nil {
        fmt.Println("%v.RouteChat(_) = _, %v", client, err)
    }
    waitc := make(chan struct{})
    go func() {
        for {
            in, err := stream.Recv()
            if err == io.EOF {
                // read done.
                fmt.Println("read done ")
                close(waitc)
                return
            }
            if err != nil {
                fmt.Println("Failed to receive a note : %v", err)
            }
            fmt.Println("Got message %s at point(%d, %d)",in.Count,in.Sex,in.Age,in.Name)
        }
    }()
    fmt.Println("notes",notes)
    for _, note := range notes {
        if err := stream.Send(note); err != nil {
            fmt.Println("Failed to send a note: %v", err)
        }
    }
    stream.CloseSend()
    <-waitc
}

完成之後我們就可以編譯啦go run mian.go/client.go
測試結果如下服務端:
這裏寫圖片描述
客戶端:
這裏寫圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章