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 TypeC++JavaPythonGo
doubledoubledoublefloatfloat64
floatfloatfloatfloatfloat32
int32int32intintint
int64int64longint/long[3]int64
uint32uint32int[1]int/long[3]uint32
uint64uint64long[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
测试结果如下服务端:
这里写图片描述
客户端:
这里写图片描述

©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页