Dive into gRPC(4):Streaming

在实现一个服务中,我们使用的是普通的RPC模式,即客户端传递一个Request,服务器响应一个Response。但是在有些场景中,这样的模式并不适用。

当业务需要传输大量的数据时(或者客户端向服务器传输大量数据,或者反之,或者双向需要传输大量数据),数据的传输时间可能有些长,接收端需要收到所有的数据后才能继续处理,而不能一边接收数据一边处理数据。

而gRPC除了普通的Request/Response外,提供了更加丰富的API接口,即Streaming的调用方式。从传输Streaming数据的角度来看,一共有三种:Sever-side streaming RPC、Client-side streaming RPC和Bidirectional streaming RPC。在这篇文章中,我们在我们原来的SimpleMath的基础上,通过一些例子,来演示Streaming RPC的使用。

1. Server-side Streaming

通过使用Streaming,我们可以向服务器或客户端发送一批数据,当服务器或客户端在接收这些数据的时候,不需要等到所有的消息都接收完毕后进行处理,而是可以在收到第一个消息后就开始进行处理,这显然比传统的模式具有更快的响应速度,从而提高性能。

1.1 服务定义

现在我们考虑这样的接口,接收一个参数(n>0),返回斐波那契数列的前n项。在这个简单的服务中,我们的服务器需要向客户端返回n个数,虽然有其它可选的方案(比如repeat),但我们这里使用Streaming来完成。

按照之前的步骤,首先通过.proto文件定义接口。和普通的RPC类似,唯一的不同在于,Streaming RPC有一个stream标记:

// messages for fibonacci
message FibonacciRequest {
    int32 count = 1;
}

message FibonacciResponse {
    int32 result = 1;
}

service SimpleMath {
    rpc GreatCommonDivisor (GCDRequest) returns (GCDResponse) {}
    rpc GetFibonacci (FibonacciRequest) returns (stream FibonacciResponse) {}
}

我们在这里定义了两个message还有一个rpc函数GetFibonacciFibonacciRequest表示一个RPC请求,其中的count指定了我们需要返回数列的字数数量;FibonacciResponse表示返回数列中的数字。

一个良好的惯例就是,对于每一个RPC调用,都单独定义一对RequestResponse

注意stream标记的位置。在service中,我们在returns里加入了stream标记,表明这是一个服务器端的Streaming RPC。

定义完成后就可以使用protoc编译了,具体的编译命令还需要牢记于心。

编译完成的代码里包含了Streaming的处理,其中的一些区别我们后面介绍。

1.2 修改服务端代码

之后我们完成服务器端的代码,完成我们GetFibonacci服务的实现。代码如下(文件simplemath/server/rpcimpl/simplemath.go):

func (sms *SimpleMathServer) GetFibonacci(in *pb.FibonacciRequest, stream pb.SimpleMath_GetFibonacciServer) error {
    a, b := 0, 1
    for i := 0; i < int(in.Count); i++ {
        stream.Send(&pb.FibonacciResponse{Result: int32(a)})
        a, b = b, a+b
    }
    return nil
}

1.3 有什么不同?

GreatCommonDivisor不同的是,GetFibonacci函数的参数没有了context.Context,但是多了一个pb.SimpleMath_GetFibonacciServer;并且返回值里也没有了pb.FibonacciResponse,而是在函数里面通过Send发送的。

这就是Streaming和普通RPC的一些不同。在编译simplemath.proto文件生成的simplemath.pb.go文件中,我们注意到这些细节(和Server相关的部分):

// SimpleMathServer is the server API for SimpleMath service.
type SimpleMathServer interface {
    GreatCommonDivisor(context.Context, *GCDRequest) (*GCDResponse, error)
    GetFibonacci(*FibonacciRequest, SimpleMath_GetFibonacciServer) error
}

首先,生成的SimpleMathServer接口中,在GreatCommonDivisor基础上多了个函数GetFibonacci,函数除了*FibonacciRequest参数外,还有一个SimpleMath_GetFibonacciServer接口类型的参数。这个接口定义如下:

type SimpleMath_GetFibonacciServer interface {
    Send(*FibonacciResponse) error
    grpc.ServerStream
}

这个接口底层使用了grpc.ServerStream来进行数据的流式返回。接口的名字也是可以通过已有的信息组合成的:SimpleMath+_+GetFibinacci+Server,在编写自己的服务的时候,可以通过这样的形式得到。

同时,对这个接口有一个简单的实现simpleMathGetFibonacciServer

type simpleMathGetFibonacciServer struct {
    grpc.ServerStream
}

func (x *simpleMathGetFibonacciServer) Send(m *FibonacciResponse) error {
    return x.ServerStream.SendMsg(m)
}

这个struct是通过ServerStream.SendMsg()来流式返回数据的。

1.4 修改客户端代码

然后我们需要关注客户端的一些变化,主要在发送和接收数据上。代码如下:

func getGRPCConn() (conn *grpc.ClientConn, err error) {
    creds, err := credentials.NewClientTLSFromFile("../cert/server.crt", "")
    return grpc.Dial(address, grpc.WithTransportCredentials(creds))
}

func GetFibonacci(count string) {
    conn, err := getGRPCConn()
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    num, _ := strconv.ParseInt(count, 10, 32)
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()
    // generate a client
    client := pb.NewSimpleMathClient(conn)
    // call the GetFibonacci function
    stream, err := client.GetFibonacci(ctx, &pb.FibonacciRequest{Count: int32(num)})
    if err != nil {
        log.Fatalf("could not compute: %v", err)
    }
    i := 0
    // receive the results
    for {
        result, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Fatalf("failed to recv: %v", err)
        }
        log.Printf("#%d: %d\n", i+1, result.Result)
        i++
    }
}

1.5 客户端,改变呢?

创建连接并生成一个client和之前的一样,然后通过这个client来调用相应的函数。在我们这个例子中就是GetFibonacci。只是这个函数返回的不再是之前的一个Responsemessage,而是一个SimpleMath_GetFibonacciClient。我们可以在simplemath.proto文件生成的simplemath.pb.go文件中看到具体的不同:

func (c *simpleMathClient) GetFibonacci(ctx context.Context, in *FibonacciRequest, opts ...grpc.CallOption) (SimpleMath_GetFibonacciClient, error) {
    stream, err := c.cc.NewStream(ctx, &_SimpleMath_serviceDesc.Streams[0], "/api.SimpleMath/GetFibonacci", opts...)
    if err != nil {
        return nil, err
    }
    x := &simpleMathGetFibonacciClient{stream}
    if err := x.ClientStream.SendMsg(in); err != nil {
        return nil, err
    }
    if err := x.ClientStream.CloseSend(); err != nil {
        return nil, err
    }
    return x, nil
}

这个就是这个函数的定义。可以看出,这个函数返回的是SimpleMath_GetFibonacciClient,函数的里面生成一个stream,并构造了一个和simpleMathGetFibonacciServer对应的simpleMath_GetFibonacciClient。和服务器通过ServerStream.SendMsg一样,客户端也通过ClientStream.SendMsg发送请求数据。关于SimpleMath_GetFibonacciClient接口和相应实现类型的定义如下:

type SimpleMath_GetFibonacciClient interface {
    Recv() (*FibonacciResponse, error)
    grpc.ClientStream
}

type simpleMathGetFibonacciClient struct {
    grpc.ClientStream
}

服务器通过一个循环调用Send函数发送数据,那么客户端就需要循环调用Recv函数来接收数据。Recv函数如下:

func (x *simpleMathGetFibonacciClient) Recv() (*FibonacciResponse, error) {
    m := new(FibonacciResponse)
    if err := x.ClientStream.RecvMsg(m); err != nil {
        return nil, err
    }
    return m, nil
}

底层其实是通过ClientStream.RecvMsg函数实现的。

相应的,在main.go中加入对应的case:

    case "fibo":
        if len(os.Args) < 3 {
            usage()
            os.Exit(1)
        }
        rpc.GetFibonacci(os.Args[2])

1.6 跑一下

代码编写完后,编译,然后就可以运行了。首先启动服务器,然后在客户端的目录下(simplemath/client)执行:

$ ./client fibo 10

结果如下:

2018/09/28 22:35:32 #1: 0
2018/09/28 22:35:32 #2: 1
2018/09/28 22:35:32 #3: 1
2018/09/28 22:35:32 #4: 2
2018/09/28 22:35:32 #5: 3
2018/09/28 22:35:32 #6: 5
2018/09/28 22:35:32 #7: 8
2018/09/28 22:35:32 #8: 13
2018/09/28 22:35:32 #9: 21
2018/09/28 22:35:32 #10: 34

完美。

2. Client-side Streaming

接下来我们看看客户端是Streaming的情况,这些通过Streaming方式发送的对象,和上面的一样,是同样的对象。

2.1 定义服务

首先还是定义我们的服务。这次我们通过命令行的形式输入一个数字n,然后随机生成n个正整数(在0到100之间),我们让服务器来对这些数字做个统计,包括一共有多少个数、平均值、最大值和最小值等。

可以看出,这时我们的客户端通过Streaming的方式发送多个请求,服务器收到后开始统计,最后将统计结果返回。

我们的simplemath.proto定义如下:

// messages for statistics
message StatisticsRequest {
    int32 number = 1;
}

message StatisticsResponse {
    int32 count = 1;
    int32 maximum = 2;
    int32 minimum = 3;
    float average = 4;
}

service SimpleMath {
    rpc GreatCommonDivisor (GCDRequest) returns (GCDResponse) {}
    rpc GetFibonacci (FibonacciRequest) returns (stream FibonacciResponse) {}
    rpc Statistics (stream StatisticsRequest) returns (StatisticsResponse) {}
}

Statistics函数中,和GetFibonacci类似,通过stream标记来表明通过Streaming的方式发送对象,只不过我们在参数的前面添加这个标记(stream StatisticsRequest)。

同样,使用protoc命令进行编译。

2.2 修改服务端代码

然后我们开始编写我们的服务器端代码。在simple/server/rpcimpl/simplemath.go中,添加如下代码:

func (sms *SimpleMathServer) Statistics(stream pb.SimpleMath_StatisticsServer) error {
    var count, maximum, minimum int32
    minimum = int32((^uint32(0)) >> 1)
    maximum = -minimum - 1
    var average, sum float32
    // receive the requests
    for {
        num, err := stream.Recv()
        if err == io.EOF {
            average = sum / float32(count)
            return stream.SendAndClose(&pb.StatisticsResponse{
                Count:   count,
                Maximum: maximum,
                Minimum: minimum,
                Average: average,
            })
        }
        if err != nil {
            log.Fatalf("failed to recv: %v", err)
            return err
        }
        count++
        if maximum < num.Number {
            maximum = num.Number
        }
        if minimum > num.Number {
            minimum = num.Number
        }
        sum += float32(num.Number)
    }
}

2.3 有什么不同?

函数的参数又发生了变化。这一次我们的Statistics函数接收一个SimpleMath_StatisticsServer类型的参数。这个类型是在simplemath.proto编译后自动生成的类型。我们可以通过查看这个文件来查看具体的定义(simplemath/api/simplemath.pb.go):

type SimpleMath_StatisticsServer interface {
    SendAndClose(*StatisticsResponse) error
    Recv() (*StatisticsRequest, error)
    grpc.ServerStream
}

同样,这是一个接口,定义了SendAndCloseRecv方法,前一个用来向客户端发送结果,后一个用来接收客户端的请求。

还有一个默认实现:

type simpleMathStatisticsServer struct {
    grpc.ServerStream
}

func (x *simpleMathStatisticsServer) SendAndClose(m *StatisticsResponse) error {
    return x.ServerStream.SendMsg(m)
}

func (x *simpleMathStatisticsServer) Recv() (*StatisticsRequest, error) {
    m := new(StatisticsRequest)
    if err := x.ServerStream.RecvMsg(m); err != nil {
        return nil, err
    }
    return m, nil
}

同样,底层是通过grpc.ServerStream来实现相应功能的。

2.4 修改客户端代码

接下来修改客户端的代码(simplemath/client/rpc/simplemath.go):

func Statistics(count string) {
    conn, err := getGRPCConn()
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    client := pb.NewSimpleMathClient(conn)
    stream, err := client.Statistics(context.Background())
    if err != nil {
        log.Fatalf("failed to compute: %v", err)
    }
    num, _ := strconv.ParseInt(count, 10, 32)
    r := rand.New(rand.NewSource(time.Now().UnixNano()))
    var nums []int
    for i := 0; i < int(num); i++ {
        nums = append(nums, r.Intn(100))
    }
    s := ""
    str := ""
    for i := 0; i < int(num); i++ {
        str += s + strconv.Itoa(nums[i])
    }
    log.Printf("Generate numbers: " + str)
    for _, n := range nums {
        if err := stream.Send(&pb.StatisticsRequest{Number: int32(n)}); err != nil {
            log.Fatalf("failed to send: %v", err)
        }
    }
    result, err := stream.CloseAndRecv()
    if err != nil {
        log.Fatalf("failed to recv: %v", err)
    }
    log.Printf("Count: %d\n", result.Count)
    log.Printf("Max: %d\n", result.Maximum)
    log.Printf("Min: %d\n", result.Minimum)
    log.Printf("Avg: %f\n", result.Average)
}

2.5 客户端,改变呢?

我们通过client来调用相应接口(Statistics)时,只传进了一个参数。这个接口在我们生成的.pb.go文件中定义如下:

func (c *simpleMathClient) Statistics(ctx context.Context, opts ...grpc.CallOption) (SimpleMath_StatisticsClient, error) {
    stream, err := c.cc.NewStream(ctx, &_SimpleMath_serviceDesc.Streams[1], "/api.SimpleMath/Statistics", opts...)
    if err != nil {
        return nil, err
    }
    x := &simpleMathStatisticsClient{stream}
    return x, nil
}

可以看到,这个函数返回了一个SimpleMath_StatisticsClient类型的值。这是一个interface类型,定义如下:

type SimpleMath_StatisticsClient interface {
    Send(*StatisticsRequest) error
    CloseAndRecv() (*StatisticsResponse, error)
    grpc.ClientStream
}

定义了SendCloseAndRecv两个方法,前者发送请求,后者接收服务器的多个响应。默认实现:

type simpleMathStatisticsClient struct {
    grpc.ClientStream
}

func (x *simpleMathStatisticsClient) Send(m *StatisticsRequest) error {
    return x.ClientStream.SendMsg(m)
}

func (x *simpleMathStatisticsClient) CloseAndRecv() (*StatisticsResponse, error) {
    if err := x.ClientStream.CloseSend(); err != nil {
        return nil, err
    }
    m := new(StatisticsResponse)
    if err := x.ClientStream.RecvMsg(m); err != nil {
        return nil, err
    }
    return m, nil
}

同样,底层是通过grpc.ClientStream实现的。

最后,在我们的main.go里加入相应的case:

    case "stat":
        if len(os.Args) < 3 {
            usage()
            os.Exit(1)
        }
        rpc.Statistics(os.Args[2])

2.6 再跑一下

代码编写完成后,就可以编译了。然后我们可以再执行一下:

$ ./client stat 10

结果如下:

2018/09/30 15:14:24 Generate numbers: 14 16 61 39 39 28 10 49 59 99
2018/09/30 15:14:24 Count: 10
2018/09/30 15:14:24 Max: 99
2018/09/30 15:14:24 Min: 10
2018/09/30 15:14:24 Avg: 41.400002

完美+1。

3. Bidirectional Streaming

最后,我们将上面的两个例子整合,就变成了一个双向的Streaming。

3.1 定义服务

先定义服务。我们假设客户端有多个正整数(通过命令行传入n)需要分解质因子,服务器可以接收多个数据,对每个数据进行质因子分解,然后将这个数分解的结果返回给客户端。这样,我们就得到了一个双向Streaming的例子(虽然很勉强)。

由于双向的其实就是上面两个类型的组合,这里我们简单说说就好。

simplemath.proto定义如下:

// messages for prime factorization
message PrimeFactorizationRequest {
    int32 number = 1;
}

message PrimeFactorizationResponse {
    string result = 1;
}

service SimpleMath {
    rpc GreatCommonDivisor (GCDRequest) returns (GCDResponse) {}
    rpc GetFibonacci (FibonacciRequest) returns (stream FibonacciResponse) {}
    rpc Statistics (stream StatisticsRequest) returns (StatisticsResponse) {}
    rpc PrimeFactorization (stream PrimeFactorizationRequest) returns (stream PrimeFactorizationResponse) {}
}

可以看出,这里我们的stream标记在Request和Response前面都有,表明这是一个双向Streaming。

之后通过protoc编译。

3.2 修改服务器代码

修改服务器端代码:

func (sms *SimpleMathServer) PrimeFactorization(stream pb.SimpleMath_PrimeFactorizationServer) error {
    for {
        in, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            log.Fatalf("failed to recv: %v", err)
            return err
        }
        stream.Send(&pb.PrimeFactorizationResponse{Result: primeFactorization(int(in.Number))})
    }
    return nil
}

func primeFactorization(num int) string {
    if num <= 2 {
        return strconv.Itoa(num)
    }
    n := num
    prefix := ""
    result := ""
    for i := 2; i <= n; i++ {
        for n != i {
            if n%i == 0 {
                result += prefix + strconv.Itoa(i)
                prefix = " * "
                n /= i
            } else {
                break
            }
        }
    }
    if result == "" {
        result = "1"
    }
    result = " = " + result + " * " + strconv.Itoa(n)
    return strconv.Itoa(num) + result
}

3.3 有什么不同?

我们的函数只接受一个SimpleMath_PrimeFactorizationServer类型的参数,通过这个参数在一个for循环中通过Recv函数接收Request,处理后通过Send函数发送Response。

type SimpleMath_PrimeFactorizationServer interface {
    Send(*PrimeFactorizationResponse) error
    Recv() (*PrimeFactorizationRequest, error)
    grpc.ServerStream
}

type simpleMathPrimeFactorizationServer struct {
    grpc.ServerStream
}

func (x *simpleMathPrimeFactorizationServer) Send(m *PrimeFactorizationResponse) error {
    return x.ServerStream.SendMsg(m)
}

func (x *simpleMathPrimeFactorizationServer) Recv() (*PrimeFactorizationRequest, error) {
    m := new(PrimeFactorizationRequest)
    if err := x.ServerStream.RecvMsg(m); err != nil {
        return nil, err
    }
    return m, nil
}

同样,是通过grpc.ServerStream实现的。

3.4 修改客户端代码

修改客户端代码:

func PrimeFactorization(count string) {
    conn, err := getGRPCConn()
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()
    client := pb.NewSimpleMathClient(conn)
    stream, err := client.PrimeFactorization(context.Background())
    if err != nil {
        log.Fatalf("failed to compute: %v", err)
    }
    waitc := make(chan struct{})

    go func() {
        for {
            in, err := stream.Recv()
            if err == io.EOF {
                close(waitc)
                return
            }
            }
            if err != nil {
                log.Fatalf("failed to recv: %v", err)
            }
            log.Printf(in.Result)
        }
    }()

    num, _ := strconv.ParseInt(count, 10, 32)
    r := rand.New(rand.NewSource(time.Now().UnixNano()))
    var nums []int
    for i := 0; i < int(num); i++ {
        nums = append(nums, r.Intn(1000))
    }
    for _, n := range nums {
        if err := stream.Send(&pb.PrimeFactorizationRequest{Number: int32(n)}); err != nil {
            log.Fatalf("failed to send: %v", err)
        }
        log.Printf("send number: %d",n)
    }
    stream.CloseSend()
    <-waitc
}

3.5 客户端,改变呢?

这里我们使用两个for循环,分别对应发送请求和接收响应。可以看到,这就是我们前面两个部分的组合。

在生成的.pb.go文件中,可以看到底层的实现:

func (c *simpleMathClient) PrimeFactorization(ctx context.Context, opts ...grpc.CallOption) (SimpleMath_PrimeFactorizationClient, error) {
    stream, err := c.cc.NewStream(ctx, &_SimpleMath_serviceDesc.Streams[2], "/api.SimpleMath/PrimeFactorization", opts...)
    if err != nil {
        return nil, err
    }
    x := &simpleMathPrimeFactorizationClient{stream}
    return x, nil
}

type SimpleMath_PrimeFactorizationClient interface {
    Send(*PrimeFactorizationRequest) error
    Recv() (*PrimeFactorizationResponse, error)
    grpc.ClientStream
}

type simpleMathPrimeFactorizationClient struct {
    grpc.ClientStream
}

func (x *simpleMathPrimeFactorizationClient) Send(m *PrimeFactorizationRequest) error {
    return x.ClientStream.SendMsg(m)
}

func (x *simpleMathPrimeFactorizationClient) Recv() (*PrimeFactorizationResponse, error) {
    m := new(PrimeFactorizationResponse)
    if err := x.ClientStream.RecvMsg(m); err != nil {
        return nil, err
    }
    return m, nil
}

也是通过grpc.ClientStream实现的。

最后,在main.go中添加相应的case:

    case "prime":
        if len(os.Args) < 3 {
            usage()
            os.Exit(1)
        }
        rpc.PrimeFactorization(os.Args[2])

3.6 再再跑一下

编译运行,再来一次:

$ ./client prime 10

结果如下:

2018/09/30 16:34:40 send number: 116
2018/09/30 16:34:40 send number: 990
2018/09/30 16:34:40 send number: 664
2018/09/30 16:34:40 send number: 262
2018/09/30 16:34:40 send number: 403
2018/09/30 16:34:40 send number: 454
2018/09/30 16:34:40 send number: 311
2018/09/30 16:34:40 send number: 191
2018/09/30 16:34:40 send number: 843
2018/09/30 16:34:40 send number: 744
2018/09/30 16:34:40 116 = 2 * 2 * 29
2018/09/30 16:34:40 990 = 2 * 3 * 3 * 5 * 11
2018/09/30 16:34:40 664 = 2 * 2 * 2 * 83
2018/09/30 16:34:40 262 = 2 * 131
2018/09/30 16:34:40 403 = 13 * 31
2018/09/30 16:34:40 454 = 2 * 227
2018/09/30 16:34:40 311 = 1 * 311
2018/09/30 16:34:40 191 = 1 * 191
2018/09/30 16:34:40 843 = 3 * 281
2018/09/30 16:34:40 744 = 2 * 2 * 2 * 3 * 31

完美+2。

4. 小结一哈

我们首先看一下生成的simplemath.pb.go文件中的SimpleMathClientSimpleMathServer的定义:

type SimpleMathClient interface {
    GreatCommonDivisor(ctx context.Context, in *GCDRequest, opts ...grpc.CallOption) (*GCDResponse, error)
    GetFibonacci(ctx context.Context, in *FibonacciRequest, opts ...grpc.CallOption) (SimpleMath_GetFibonacciClient, error)
    Statistics(ctx context.Context, opts ...grpc.CallOption) (SimpleMath_StatisticsClient, error)
    PrimeFactorization(ctx context.Context, opts ...grpc.CallOption) (SimpleMath_PrimeFactorizationClient, error)
}

这四个函数分别对应着四种方式,即普通、Server-side Streaming、Client-side Streaming和Bidirectional Streaming。从这四个函数的定义我们就能看出四种方式的不同:

  • 普通方式:函数接收一个Request参数,返回一个Response;
  • Server-side Streaming:函数接收一个Request,返回一个XXXClient;
  • Client-side Streaming:函数返回一个XXXClient;
  • Bidirectional Streaming:函数返回一个XXXClient。

可见,只要涉及到Stream,函数就需要一XXXClient,并通过这个XXXClient的Send发送多个Request,通过Recv接收多个Response。

这些函数的定义影响到我们在客户端调用RPC接口时的行为,需要注意。

然后我们看看SimpleMathServer的定义:

type SimpleMathServer interface {
    GreatCommonDivisor(context.Context, *GCDRequest) (*GCDResponse, error)
    GetFibonacci(*FibonacciRequest, SimpleMath_GetFibonacciServer) error
    Statistics(SimpleMath_StatisticsServer) error
    PrimeFactorization(SimpleMath_PrimeFactorizationServer) error
}

同样,四个函数对应四种方式:

  • 普通方式:函数接收一个Request,返回一个Response;
  • Server-side Streaming:函数接收一个Request,返回一个XXXServer;
  • Client-side Streaming:函数只接收一个XXXServer;
  • Bidirectional Streaming:函数只接收一个XXXServer。

同理,只要涉及到Stream,函数就需要一个XXXServer,并通过这个XXXServer的Send函数发送多个Response,通过Recv函数接收多个Request。

这些函数的定义影响到我们在服务器端定义RPC接口逻辑时的行为,也需要注意。

从上面可以看出,说到底,不同的方式对应着Request和Response不同的接收与发送方式。我们通过下表来进行总结:

方式 Client发送Request Server接收Request Client接收Response Server发送Response
普通 函数参数传入 函数参数传入 函数结果返回 函数结果返回
SS 函数参数传入 函数参数传入 XXXClient.Recv() XXXServer.Send()
CS XXXClient.Send() XXXServer.Recv() XXXClient.CloseAndRecv() XXServer.SendAndClose()
BS XXXClient.Send() XXXServer.Recv() XXXClient.Recv() XXXServer.Send()

拖拖拉拉写了这么多,也只是表面的东西,有机会更深入地研究一下。

To Be Continued~

5. 系列文章

  • Dive into gRPC(1):gRPC简介
  • Dive into gRPC(2):实现一个服务
  • Dive into gRPC(3):安全通信
  • Dive into gRPC(4):Streaming
  • Dive into gRPC(5):验证客户端
  • Dive into gRPC(6):metadata

你可能感兴趣的:(Dive into gRPC(4):Streaming)