在实现一个服务中,我们使用的是普通的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函数GetFibonacci
。FibonacciRequest
表示一个RPC请求,其中的count
指定了我们需要返回数列的字数数量;FibonacciResponse
表示返回数列中的数字。
一个良好的惯例就是,对于每一个RPC调用,都单独定义一对
Request
和Response
。
注意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
}
同样,这是一个接口,定义了SendAndClose
和Recv
方法,前一个用来向客户端发送结果,后一个用来接收客户端的请求。
还有一个默认实现:
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
}
定义了Send
和CloseAndRecv
两个方法,前者发送请求,后者接收服务器的多个响应。默认实现:
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
文件中的SimpleMathClient
和SimpleMathServer
的定义:
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