gRPC入门


1. 什么是gRPC

gRPC是谷歌公司基于protobuf开发的跨语言的开源RPC框架,基于http/2协议设计,对移动设备更加友好。

go语言gRPC技术栈最底层为TCP或者Unix套接字协议,在此之上是http/2协议的实现,然后在http/2协议之上有构建了针对go语言的gRPC核心库。应用程序通过gRPC插件生成的Stub代码和gRPC核心库通信,也可以直接和gRPC核心库通信。

2. gRPC入门

创建hello.proto文件,定义需要的HelloService接口:

syntax="proto3";
package pb;
option go_package="../pb";

message String{
  string value=1;
}

service HelloService{
  rpc Hello (String) returns (String);
}

使用gRPC生成指令

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

我们可以在生成的代码文件中看到gRPC插件会为服务器和客户端生成不同的接口

gRPC通过context参数,为每个方法调用提供了上下文支持。客户端在调用方法的时候,可以通过可选的CallOption类型的参数提供额外的上下文信息。

我们可以构建一个gRPC的客户端:

type HelloServiceImpl struct {
}

func (p *HelloServiceImpl) Hello(ctx context.Context, args *pb.String) (*pb.String, error) {
    reply := &pb.String{Value: "hello: " + args.GetValue()}
    return reply, nil
}

func main() {
    // 构造一个grpc服务对象
    grpcServer := grpc.NewServer()
    // 注册grpc服务,和rpc很类似
    pb.RegisterHelloServiceServer(grpcServer, new(HelloServiceImpl))

    listener, err := net.Listen("tcp", ":1234")
    if err != nil {
        log.Fatal(err)
    }

    // 在指定端口提供grpc服务
    grpcServer.Serve(listener)
}

然后构建一个gRPC客户端接收gRPC服务:

func main() {
    // 和grpc服务建立连接
    conn, err := grpc.Dial("localhost:1234",
        grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // 构造client对象,client可以调用服务端注册的grpc服务提供的方法
    client := pb.NewHelloServiceClient(conn)

    // 调用grpc服务
    reply, err := client.Hello(context.Background(), &pb.String{Value: "hello"})
    if err != nil {
        log.Fatal(err)
    }
    fmt.Println(reply.GetValue())
}

现在就可以启用grpc服务了

gRPC和标准库RPC有一个区别,即gRPC生成的接口并不支持异步调用。不过我们可以在多个goroutine之间安全的共享gRPC底层的http/2链接,因此可以通过在另一个goroutine阻塞调用的方法模拟异步调用。

3. gRPC流

远程调用要求每次传输的数据量不能太大,否则响应时间带来的问题将损害rpc的高效性,所以传统rpc对上传下载数据量较大的场景并不合适,同时传统rpc也不适用于时间不确定的订阅和发布模式。

为此,gRPC框架针对服务端和客户端分别提供了流特性。

服务端或客户端的单项流是双向流的特例,我们在HelloService增加一个支持双向流的Channel()方法

关键字stream指定启用的流特性,参数部分是接受客户端的流,返回值是返回给客户端的流。

service HelloService{
  rpc Hello (String) returns (String);
  rpc Channel(stream String) returns (stream String);
}

重新生成代码,可以看到接口中新增的Channel()方法的定义

现在我们可以开始实现服务端流服务,基本的方法就是Recv() 和Send()

func (p *HelloServiceImpl) Channel(stream pb.HelloService_ChannelServer) error {

    // 循环接收客户端发来的数据
    for {
        args, err := stream.Recv()
        if err != nil {
            // 如果遇到EOF表示客户端流关闭
            if err == io.EOF {
                return nil
            }
            return err
        }

        // 生成数据
        reply := &pb.String{Value: "hello: " + args.GetValue()}

        // 通过流发送给客户端
        err = stream.Send(reply)
        if err != nil {
            return err
        }
    }
}

然后让客户端在两个不同的协程中处理接收和发送:

func main() {
    // 和grpc服务建立连接
    conn, err := grpc.Dial("localhost:1234",
        grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatal(err)
    }
    defer conn.Close()

    // 构造client对象,client可以调用服务端注册的grpc服务提供的方法
    client := pb.NewHelloServiceClient(conn)

    stream, err := client.Channel(context.Background())
    if err != nil {
        log.Fatal(err)
    }

    // 将发送和接收数据放到两个独立的goroutine中
    // 发送数据
    go func() {
        for {
            if err := stream.Send(&pb.String{Value: "hi"}); err != nil {
                log.Fatal(err)
            }
            time.Sleep(time.Second)
        }
    }()

    // 接收数据
    for {
        reply, err := stream.Recv()
        if err != nil {
            if err == io.EOF {
                break
            }
            log.Fatal(err)
        }
        fmt.Println(reply.GetValue())
    }
}