gRPC之gRPC流

1、gRPC流

从其名称可以理解,流就是持续不断的传输。有一些业务场景请求或者响应的数据量比较大,不适合使用普通的

RPC 调用通过一次请求-响应处理,一方面是考虑数据量大对请求响应时间的影响,另一方面业务场景的设计不一

定需要一次性处理完所有数据,这时就可以使用流来分批次传输数据。

HTTP/2中有两个概念,流(stream)与帧(frame),其中帧作为HTTP/2中通信的最小传输单位,通常一个请

求或响应会被分为一个或多个帧传输,流则表示已建立连接的虚拟通道,可以传输多次请求或响应。每个帧中包含

Stream Identifier,标志所属流。HTTP/2通过流与帧实现多路复用,对于相同域名的请求,通过Stream

Identifier标识可在同一个流中进行,从而减少连接开销。 而gRPC基于HTTP/2协议传输,自然而然也实现了流式

传输,其中gRPC中共有以下三种类型的流:

1、服务端流式响应

2、客户端流式请求

3、两端双向流式

本篇主要讲讲如何实现gRPC三种流式处理。

gRPC的stream只需要在service的rpc方法描述中通过 stream 关键字指定启用流特性就好了。

1.1 单向流

单向流是指客户端和服务端只有一端开启流特性,这里的单向特指发送数据的方向。

  • 当服务端开启流时,客户端和普通 RPC 调用一样通过一次请求发送数据,服务端通过流分批次响应。

  • 当客户端开启流时,客户端通过流分批次发送请求数据,服务端接完所有数据后统一响应一次。

1.1.1 服务端流

定义一个 MultiPong 方法,在服务端开启流,功能是接收到客户端的请求后响应10次 pong 消息。

ping.proto文件的编写:

protobuf 复制代码
// ping.proto
// 指定proto版本
syntax = "proto3";
// 指定包名
package protos;
// 指定go包路径
option go_package = "protos/ping";
// 定义PingPong服务
service PingPong {
    // Ping发送ping请求,接收pong响应
    // 服务端流模式,在响应消息前添加stream关键字
    rpc MultiPong(PingRequest) returns (stream PongResponse);
}

// PingRequest请求结构
message PingRequest {
	// value字段为string类型
    string value = 1; 
}

// PongResponse 响应结构
message PongResponse {
	// value字段为string类型
    string value = 1; 
}

ping.pb.go文件的生成:

shell 复制代码
$ protoc --go_out=plugins=grpc:. ping.proto

服务端实现,server.go的编写,第二个参数为 stream 对象的引用,可以通过它的 Send 方法发送数据。

go 复制代码
package main

import (
	// 引入编译生成的包
	pb "demo/protos/ping"
	"google.golang.org/grpc"
	"log"
	"net"
)

// PingPongServer 实现 pb.PingPongServer 接口
type PingPongServer struct {
	pb.UnimplementedPingPongServer
}

// MultiPong 服务端流模式
func (s *PingPongServer) MultiPong(req *pb.PingRequest, stream pb.PingPong_MultiPongServer) error {
	for i := 0; i < 10; i++ {
		data := &pb.PongResponse{Value: "pong"}
		// 发送消息
		err := stream.Send(data)
		if err != nil {
			return err
		}
	}
	return nil
}

// 启动server
func main() {
	srv := grpc.NewServer()
	// 注册 PingPongServer
	pb.RegisterPingPongServer(srv, &PingPongServer{})
	lis, err := net.Listen("tcp", ":7009")
	if err != nil {
		log.Fatalln(err)
	}
	log.Println("listen on 7009")
	srv.Serve(lis)
}
shell 复制代码
# 启动server
$ go run server.go
2023/02/10 20:51:04 listen on 7009

客户端实现,client.go的编写,请求方式和普通 RPC 没有区别,重点关注对响应数据流的处理,通过一个 for

循环接收数据直到结束。

go 复制代码
package main

import (
	"context"
	pb "demo/protos/ping" // 引入编译生成的包
	"google.golang.org/grpc"
	"io"
	"log"
)

// Ping 单次请求-响应模式
func main() {
	conn, err := grpc.Dial("localhost:7009", grpc.WithInsecure())
	if err != nil {
		log.Fatalln(err)
	}
	defer conn.Close()
	// 实例化客户端并调用
	client := pb.NewPingPongClient(conn)
	// 获得对 stream 对象的引用
	stream, err := client.MultiPong(context.Background(), &pb.PingRequest{Value: "ping"})
	if err != nil {
		log.Fatalln(err)
	}
	// 循环接收响应数据流
	for {
		msg, err := stream.Recv()
		if err != nil {
			// 数据结束
			if err == io.EOF {
				break
			}
			log.Fatalln(err)
		}
		log.Println(msg.Value)
	}
}
shell 复制代码
# 客户端运行
$ go run client.go
2023/02/10 20:54:34 pong
2023/02/10 20:54:34 pong
2023/02/10 20:54:34 pong
2023/02/10 20:54:34 pong
2023/02/10 20:54:34 pong
2023/02/10 20:54:34 pong
2023/02/10 20:54:34 pong
2023/02/10 20:54:34 pong
2023/02/10 20:54:34 pong
2023/02/10 20:54:34 pong
shell 复制代码
# 目录结构
$ tree demo
demo
├── client.go
├── go.mod
├── go.sum
├── ping.proto
├── protos
│   └── ping
│       └── ping.pb.go
└── server.go

2 directories, 6 files

1.1.2 客户端流

定义一个 MultiPing 方法,在客户端开启流,功能是持续发送多个 ping 请求,服务端统一响应一次。

ping.proto文件的编写:

go 复制代码
// ping.proto
// 指定proto版本
syntax = "proto3"; 
// 指定包名
package protos;     
// 指定go包路径
option go_package = "protos/ping";
// 定义PingPong服务
service PingPong {
    // Ping 发送 ping 请求,接收 pong 响应
    // 客户端流模式,在请求消息前添加 stream 关键字
    rpc MultiPing(stream PingRequest) returns (PongResponse);
}

// PingRequest 请求结构
message PingRequest {
    string value = 1; // value字段为string类型
}

// PongResponse 响应结构
message PongResponse {
    string value = 1; // value字段为string类型
}

ping.pb.go文件的生成:

shell 复制代码
$ protoc --go_out=plugins=grpc:. ping.proto

服务端实现,server.go的编写,只有一个参数为 stream 对象的引用,可以通过它的 Recv 方法接收数据。使

SendAndClose 方法关闭流并响应,服务端可以根据需要提前关闭。

go 复制代码
package main

import (
	"fmt"
	// 引入编译生成的包
	pb "demo/protos/ping"
	"google.golang.org/grpc"
	"io"
	"log"
	"net"
)

// PingPongServer 实现 pb.PingPongServer 接口
type PingPongServer struct {
	pb.UnimplementedPingPongServer
}

// MultiPing 客户端流模式
func (s *PingPongServer) MultiPing(stream pb.PingPong_MultiPingServer) error {
	msgs := []string{}
	for {
		// 提前结束接收消息
		if len(msgs) > 5 {
			return stream.SendAndClose(&pb.PongResponse{Value: "ping enough, max 5"})
		}
		msg, err := stream.Recv()
		if err != nil {
			// 客户端消息结束,返回响应信息
			if err == io.EOF {
				return stream.SendAndClose(&pb.PongResponse{Value: fmt.Sprintf("got %d ping", len(msgs))})
			}
			return err
		}
		msgs = append(msgs, msg.Value)
	}
}

// 启动server
func main() {
	srv := grpc.NewServer()
	// 注册 PingPongServer
	pb.RegisterPingPongServer(srv, &PingPongServer{})
	lis, err := net.Listen("tcp", ":7009")
	if err != nil {
		log.Fatalln(err)
	}
	log.Println("listen on 7009")
	srv.Serve(lis)
}
shell 复制代码
# 启动server
$ go run server.go
2023/02/10 21:26:42 listen on 7009

客户端实现,client.go的编写,调用 MultiPing 方法时不再指定请求参数,而是通过返回的 stream 对象的

Send 分批发送数据。

go 复制代码
package main

import (
	"context"
	pb "demo/protos/ping" // 引入编译生成的包
	"google.golang.org/grpc"
	"log"
)

// Ping 单次请求-响应模式
func main() {
	conn, err := grpc.Dial("localhost:7009", grpc.WithInsecure())
	if err != nil {
		log.Fatalln(err)
	}
	defer conn.Close()
	// 实例化客户端并调用
	client := pb.NewPingPongClient(conn)
	// 获得对stream对象的引用
	// 调用并得到stream对象
	stream, err := client.MultiPing(context.Background())
	if err != nil {
		log.Fatalln(err)
	}
	// 发送数据
	for i := 0; i < 6; i++ {
		data := &pb.PingRequest{Value: "ping"}
		err = stream.Send(data)
		if err != nil {
			log.Fatalln(err)
		}
	}
	// 发送结束并获取服务端响应
	res, err := stream.CloseAndRecv()
	if err != nil {
		log.Fatalln(err)
	}
	log.Println(res.Value)
}
shell 复制代码
# 启动客户端
# 发送3个ping
$ go run client.go
2023/02/10 21:32:31 got 3 ping
# 发送6个ping
$ go run client.go
2023/02/10 21:32:31 ping enough, max 5
shell 复制代码
# 项目结构
$ tree demo
demo
├── client.go
├── go.mod
├── go.sum
├── ping.proto
├── protos
│   └── ping
│       └── ping.pb.go
└── server.go

2 directories, 6 files

1.2 双向流

双向流是指客户端在发送数据和服务端响应数据的过程中都启用流特性,实际上单向流只是双向流的特例,有了上

面的基础,双向流就很好理解了。

定义一个 MultiPingPong 方法,在客户端和服务端都开启流,功能是服务端每接收到两个 ping 就响应一次

pong。

ping.proto编写:

protobuf 复制代码
// ping.proto
// 指定proto版本
syntax = "proto3"; 
// 指定包名
package protos;     
// 指定go包路径
option go_package = "protos/ping";

// 定义PingPong服务
service PingPong {
    // Ping 发送 ping 请求,接收 pong 响应
    // 双向流模式
    rpc MultiPingPong(stream PingRequest) returns (stream PongResponse);
}

// PingRequest 请求结构
message PingRequest {
    string value = 1; // value字段为string类型
}

// PongResponse 响应结构
message PongResponse {
    string value = 1; // value字段为string类型
}

ping.pb.go文件的生成:

shell 复制代码
$ protoc --go_out=plugins=grpc:. ping.proto

服务端实现,server.go的编写,同样通过 streamRecvSend 方法接收和发送数据。

go 复制代码
package main

import (
	pb "demo/protos/ping" // 引入编译生成的包
	"google.golang.org/grpc"
	"io"
	"log"
	"net"
)

// PingPongServer 实现 pb.PingPongServer 接口
type PingPongServer struct {
	pb.UnimplementedPingPongServer
}

func (s *PingPongServer) MultiPingPong(stream pb.PingPong_MultiPingPongServer) error {
	msgs := []string{}
	for {
		// 接收消息
		msg, err := stream.Recv()
		if err != nil {
			if err == io.EOF {
				break
			}
			return err
		}
		msgs = append(msgs, msg.Value)
		// 每收到两个消息响应一次
		if len(msgs)%2 == 0 {
			err = stream.Send(&pb.PongResponse{Value: "pong"})
			if err != nil {
				return err
			}
		}
	}
	return nil
}

// 启动server
func main() {
	srv := grpc.NewServer()
	// 注册 PingPongServer
	pb.RegisterPingPongServer(srv, &PingPongServer{})
	lis, err := net.Listen("tcp", ":7009")
	if err != nil {
		log.Fatal(err)
	}
	log.Println("listen on 7009")
	srv.Serve(lis)
}
shell 复制代码
# 启动server
$ go run server.go
2023/02/10 21:26:42 listen on 7009

客户端实现,client.go的编写,这里在另外一个 goroutine 里处理接收数据的逻辑来演示同时发送和接收数

据。

go 复制代码
package main

import (
	"context"
	pb "demo/protos/ping" // 引入编译生成的包
	"google.golang.org/grpc"
	"io"
	"log"
	"time"
)

// Ping 单次请求-响应模式
func main() {
	conn, err := grpc.Dial("localhost:7009", grpc.WithInsecure())
	if err != nil {
		log.Fatal(err)
	}
	defer conn.Close()
	// 实例化客户端并调用
	client := pb.NewPingPongClient(conn)
	stream, err := client.MultiPingPong(context.Background())
	if err != nil {
		log.Fatal(err)
	}
	// 在另一个goroutine中处理接收数据
	c := make(chan struct{})
	go func(stream pb.PingPong_MultiPingPongClient, c chan struct{}) {
		defer func() {
			c <- struct{}{}
		}()
		for {
			msg, err := stream.Recv()
			if err != nil {
				if err == io.EOF {
					break
				}
				log.Fatal(err)
			}
			log.Printf("recv:%s\n", msg.Value)
		}
	}(stream, c)
	// 发送数据
	for i := 0; i < 6; i++ {
		data := &pb.PingRequest{Value: "ping"}
		err = stream.Send(data)
		if err != nil {
			log.Fatal(err)
		}
		log.Printf("send:%s\n", data.Value)

		// 延时一段时间发送,等待响应结果
		time.Sleep(500 * time.Millisecond)
	}

	// 结束发送
	stream.CloseSend()
	// 等待接收完成
	<-c
}
shell 复制代码
# 启动客户端
$ go run client.go
2023/02/10 21:48:26 send:ping
2023/02/10 21:48:26 send:ping
2023/02/10 21:48:26 recv:pong
2023/02/10 21:48:27 send:ping
2023/02/10 21:48:27 send:ping
2023/02/10 21:48:27 recv:pong
2023/02/10 21:48:28 send:ping
2023/02/10 21:48:28 send:ping
2023/02/10 21:48:28 recv:pong
shell 复制代码
# 项目结构
$ tree demo
demo
├── client.go
├── go.mod
├── go.sum
├── ping.proto
├── protos
│   └── ping
│       └── ping.pb.go
└── server.go

2 directories, 6 files
相关推荐
许野平11 天前
Rust:设计 gRPC 客户端
开发语言·后端·rust·grpc·tonic
钢铁小狗侠17 天前
如何在windows上下载和编译grpc
grpc
寒烟说25 天前
用 Go 语言实现一个最简单的 gRPC 服务端
开发语言·后端·golang·grpc
陈亦康1 个月前
Armeria gPRC 高级特性 - 装饰器、无框架请求、阻塞处理器、Nacos集成、负载均衡、rpc异常处理、文档服务......
kotlin·grpc·armeria
假装我不帅2 个月前
asp.net core grpc快速入门
后端·asp.net·grpc
cci2 个月前
Rust gRPC---Tonic教程
后端·rust·grpc
磐石区2 个月前
gRPC etcd 服务注册与发现、自定义负载均衡
服务发现·负载均衡·etcd·grpc·picker
Mindfulness code2 个月前
使用 gRPC
开发语言·grpc
crossoverJie2 个月前
OpenTelemetry 实战:gRPC 监控的实现原理
grpc·opentelemetry
czl3893 个月前
gRPC golang开发实践
golang·grpc·protobuf