目录
一. 什么是grpc
- 参考博客
- grpc是google开源的一个高性能RPC框架,基于HTTP2标准设计,支持普通的RPC也支持双向流式通信,相对于thrift,grpc可以多路复用,可以传递header头
- grpc链接的建立:
- 由于是基于http2实现的,http2又要求必须是https协议,所以与https大致相同,建立连接时除了三次握手外还存在一次tls握手
- 连接建立后会发起一个连接前言请求(Magic帧 Setting帧),
- 通过Win_up滑动窗口传输数据
- 基于Header/Data帧进行传播和设置操作
- grpc会将数据存放到Data中, Data通常采用protobuf,需要设置IDL结构体,编解码数据进行传输
- 请求执行完毕后通过四次挥手断开连接
- grpc请求数据包分析
HEADERS (flags = END_HEADERS)
:method : POST // 请求的方法
:scheme : http // http/https
:path : /Comment/AddComment // 微服务路径 /{服务名}/{服务方法}
:authority : baidu.com // 目标 URL 的虚拟主机名
te : trailers // gRPC 中 这个值必须为 trailers
grpc-timeout : 15 // 调用的超时时间
Content-Type : application/grpc // grpc 必须以 applaction/grpc 开头
grpc-encoding : gzip // 压缩类型
authorization : *****
- grpc响应数据包分析
HEADERS (flags = END_HEADERS)
:status : 200
grpc-encoding : gzip
Content-Type: application/grpc
- 注意: 消息体以二进制形式传输数据,开头是数据的长度,会以一个或多个帧来发送消息
- 请求消息结束会在最后一个数据帧上添加 END_STREAM 或发送一个带有 END_STREAM 空的数据帧
END_STREAM 也 称作 EOS(end of stream)
- 消息结束通过发送 trailer 来提醒客户端响应消息已发送
HEADERS (flags = END_STREAM, END_HEADERS)
grpc-status : 0 // OK / grcp 状态码
grpc-message : **** // 对错误的描述
二. grpc 相关插件安装
- 参考博客
- 开启go mod
go mod export GO111MODULE=on
- 开启代理
go mod export GOPROXY=https://goproxy.io
- 安装grpc
go get google.golang.org/grpc
- 安装proto,因为这些文件在安装grpc的时候,已经下载下来了,因此使用install命令就可以了,而不需要使用get命令
go install google.golang.org/protobuf/cmd/protoc-gen-go
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc
- 在执行install安装protoc-gen-go-grpc时,可能会报异常"没有必要的模块提供包",并且提示了添加获取方法,执行获取"protoc-gen-go-grp"命令
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc
- 上方命令执行完毕后会在"/GOPATH/bin/"目录下生成protoc-gen-go.exe与protoc-gen-go-grpc.exe两个可执行文件,查看这两个文件所在目录是否配置到了环境变量path中,如果没有添加到path中
- 其它手动拉取protoc-gen-go命令
go get -u github.com/golang/prorobuf/protoc-gen-go
- cmd命令行窗口执行"protoc"校验是否安装成功
- 问题:
三. 编写Proto模板,生成代码
- 创建".proto"结尾的文件,编写用来生成go文件的模板,例如创建"my_test.proto"文件
//todo 指定生成proto时用的语法版本
syntax = "proto3";
//当前文件所在包
package test;
//todo 指定生成的go文件所在目录包,"."表示在当前目录生成,"proto"表示生成的go文件的包名是proto
option go_package = ".;proto";
//todo 定义请求参数类型
message TestRequest {
//参数类型 变量名=标号
string message = 1;
}
//todo 定义响应参数类型
message TestResponse {
string message = 1;
int32 code = 2;
Corpus corpus = 4;
}
//todo 定义枚举
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
//todo 定义server
service MyGrpc {
//todo 定义接口
//通信协议 方法名(入参类型) returns (反参类型){}
rpc MethodOne(TestRequest) returns (TestResponse) {}
//定义以流式响应的接口
rpc ServerStreaming(TestRequest) returns (stream TestResponse) {}
//定义以流式接收数据的接口
rpc ClientStreaming(stream TestRequest) returns (TestResponse) {}
//定义双向数据流接口
rpc BidirectionalStreaming(stream TestRequest) returns (stream TestResponse) {}
}
- 切换到该文件所在目录,执行编译命令
protoc --go_out=. 文件名.proto
protoc --go-grpc_out=. 文件名.proto
- 编译命令执行完成后会生"文件名.pb.go"与"文件名_grpc.pb.go",在这两个文件中定义了方法的go语言实现,定义了请求与相应的go语言,就是通过protoc-gen-go把定义的语言无关的.proto转换为go语言的代码,以便server和client直接使用
- 问题: 网上很多教程中使用一下命令编译生成文件,为什么此处没有,因为下方的编译命令使用的是github版本的protoc-gen-go,而目前这个项目已经由Google接管了,并且该命令只会生成xxx.pb.go一个文件
protoc --go_out=plugins=grpc:. 文件名.proto
- protobuf 语法Go-gRPC 实践指南:Protobuf语法
四. grpc 服务器与客户端使用示例
- 基于上方.proto文件进行编译,生成server和client可以使用的go文件,先看一下"my_test.proto"文件,该文件内部定义了一个MyGrpc服务, 服务中存在4个接口,并且定义了接口执行需要的入参反参(注意不要使用proto作为文件名,可能会和标准库冲突)
- 执行编译命令
protoc --go-grpc_out=. my_test.proto
protoc --go_out=. my_test.proto
- 执行编译后会生成"文件名.pb.go"与"文件名_grpc.pb.go"两个文件,
解释"_grpc.pb.go"结尾文件
- 查看以"_grpc.pb.go"结尾的文件,此处时my_test_grpc.pb.go
- “.proto"中定义了服务与接口,在”_grpc.pb.go"文件中生成了对应服务端的服务接口定义
发现多生成了一个mustEmbedUnimplementedXxxx()接口,如果不需要该接口可以使用
“protoc --go-grpc_out=require_unimplemented_servers=false” 关闭,或者在grpc server实现结构体中匿名嵌入Unimplemented***Server结构体
- "_grpc.pb.go"文件中同时也生成了注册服务的方法
- "_grpc.pb.go"文件中同时也生成了针对服务端接口定义的实现base实现?(如果实际开发中有接口不需实现,在创建服务结构体时,结构体中内嵌base结构体即可?)
- 针对服务端的其它,例如流式处理的相关代码
func _MyGrpc_MethodOne_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(TestRequest)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(MyGrpcServer).MethodOne(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: "/test.MyGrpc/MethodOne",
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(MyGrpcServer).MethodOne(ctx, req.(*TestRequest))
}
return interceptor(ctx, in, info, handler)
}
func _MyGrpc_ServerStreaming_Handler(srv interface{}, stream grpc.ServerStream) error {
m := new(TestRequest)
if err := stream.RecvMsg(m); err != nil {
return err
}
return srv.(MyGrpcServer).ServerStreaming(m, &myGrpcServerStreamingServer{stream})
}
type MyGrpc_ServerStreamingServer interface {
Send(*TestResponse) error
grpc.ServerStream
}
type myGrpcServerStreamingServer struct {
grpc.ServerStream
}
func (x *myGrpcServerStreamingServer) Send(m *TestResponse) error {
return x.ServerStream.SendMsg(m)
}
func _MyGrpc_ClientStreaming_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(MyGrpcServer).ClientStreaming(&myGrpcClientStreamingServer{stream})
}
type MyGrpc_ClientStreamingServer interface {
SendAndClose(*TestResponse) error
Recv() (*TestRequest, error)
grpc.ServerStream
}
type myGrpcClientStreamingServer struct {
grpc.ServerStream
}
func (x *myGrpcClientStreamingServer) SendAndClose(m *TestResponse) error {
return x.ServerStream.SendMsg(m)
}
func (x *myGrpcClientStreamingServer) Recv() (*TestRequest, error) {
m := new(TestRequest)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func _MyGrpc_BidirectionalStreaming_Handler(srv interface{}, stream grpc.ServerStream) error {
return srv.(MyGrpcServer).BidirectionalStreaming(&myGrpcBidirectionalStreamingServer{stream})
}
type MyGrpc_BidirectionalStreamingServer interface {
Send(*TestResponse) error
Recv() (*TestRequest, error)
grpc.ServerStream
}
type myGrpcBidirectionalStreamingServer struct {
grpc.ServerStream
}
func (x *myGrpcBidirectionalStreamingServer) Send(m *TestResponse) error {
return x.ServerStream.SendMsg(m)
}
func (x *myGrpcBidirectionalStreamingServer) Recv() (*TestRequest, error) {
m := new(TestRequest)
if err := x.ServerStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
// MyGrpc_ServiceDesc is the grpc.ServiceDesc for MyGrpc service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var MyGrpc_ServiceDesc = grpc.ServiceDesc{
ServiceName: "test.MyGrpc",
HandlerType: (*MyGrpcServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "MethodOne",
Handler: _MyGrpc_MethodOne_Handler,
},
},
Streams: []grpc.StreamDesc{
{
StreamName: "ServerStreaming",
Handler: _MyGrpc_ServerStreaming_Handler,
ServerStreams: true,
},
{
StreamName: "ClientStreaming",
Handler: _MyGrpc_ClientStreaming_Handler,
ClientStreams: true,
},
{
StreamName: "BidirectionalStreaming",
Handler: _MyGrpc_BidirectionalStreaming_Handler,
ServerStreams: true,
ClientStreams: true,
},
},
Metadata: "my_test.proto",
}
- "_grpc.pb.go"文件中同时也生成了针对客户端的服务与接口定义
- "_grpc.pb.go"文件中同时也生成了针对客户端接口操作的实现?(可以直接拿来使用)
//客户端结构体
type myGrpcClient struct {
cc grpc.ClientConnInterface
}
//创建客户端结构体方法
func NewMyGrpcClient(cc grpc.ClientConnInterface) MyGrpcClient {
return &myGrpcClient{cc}
}
func (c *myGrpcClient) MethodOne(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (*TestResponse, error) {
out := new(TestResponse)
err := c.cc.Invoke(ctx, "/test.MyGrpc/MethodOne", in, out, opts...)
if err != nil {
return nil, err
}
return out, nil
}
func (c *myGrpcClient) ServerStreaming(ctx context.Context, in *TestRequest, opts ...grpc.CallOption) (MyGrpc_ServerStreamingClient, error) {
stream, err := c.cc.NewStream(ctx, &MyGrpc_ServiceDesc.Streams[0], "/test.MyGrpc/ServerStreaming", opts...)
if err != nil {
return nil, err
}
x := &myGrpcServerStreamingClient{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
}
- 以及客户端流式处理的相关代码
type MyGrpc_ServerStreamingClient interface {
Recv() (*TestResponse, error)
grpc.ClientStream
}
type myGrpcServerStreamingClient struct {
grpc.ClientStream
}
func (x *myGrpcServerStreamingClient) Recv() (*TestResponse, error) {
m := new(TestResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *myGrpcClient) ClientStreaming(ctx context.Context, opts ...grpc.CallOption) (MyGrpc_ClientStreamingClient, error) {
stream, err := c.cc.NewStream(ctx, &MyGrpc_ServiceDesc.Streams[1], "/test.MyGrpc/ClientStreaming", opts...)
if err != nil {
return nil, err
}
x := &myGrpcClientStreamingClient{stream}
return x, nil
}
type MyGrpc_ClientStreamingClient interface {
Send(*TestRequest) error
CloseAndRecv() (*TestResponse, error)
grpc.ClientStream
}
type myGrpcClientStreamingClient struct {
grpc.ClientStream
}
func (x *myGrpcClientStreamingClient) Send(m *TestRequest) error {
return x.ClientStream.SendMsg(m)
}
func (x *myGrpcClientStreamingClient) CloseAndRecv() (*TestResponse, error) {
if err := x.ClientStream.CloseSend(); err != nil {
return nil, err
}
m := new(TestResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
func (c *myGrpcClient) BidirectionalStreaming(ctx context.Context, opts ...grpc.CallOption) (MyGrpc_BidirectionalStreamingClient, error) {
stream, err := c.cc.NewStream(ctx, &MyGrpc_ServiceDesc.Streams[2], "/test.MyGrpc/BidirectionalStreaming", opts...)
if err != nil {
return nil, err
}
x := &myGrpcBidirectionalStreamingClient{stream}
return x, nil
}
type MyGrpc_BidirectionalStreamingClient interface {
Send(*TestRequest) error
Recv() (*TestResponse, error)
grpc.ClientStream
}
type myGrpcBidirectionalStreamingClient struct {
grpc.ClientStream
}
func (x *myGrpcBidirectionalStreamingClient) Send(m *TestRequest) error {
return x.ClientStream.SendMsg(m)
}
func (x *myGrpcBidirectionalStreamingClient) Recv() (*TestResponse, error) {
m := new(TestResponse)
if err := x.ClientStream.RecvMsg(m); err != nil {
return nil, err
}
return m, nil
}
解释".pb.go"结尾文件
- “.proto"中定义了枚举, 在”.pb.go"中生成了对应的枚举以及相关方法
- “.proto"中定义了结构体,在”.pb.go"中生成了对应的结构体以及相关方法
服务端代码示例
- 创建结构体,实现"_grpc.pb.go"中对应服务端的所有接口,创建grpc服务器,调用"_grpc.pb.go"中生成的注册服务接口,将服务实现注册到grpc服务器,监听指定端口,启动服务即可
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
"io"
"log"
"net"
)
// 1.创建服务结构体
type Server struct {
}
// 2.实现所有接口
// 2.1实现一元接口(普通接口)示例
func (s *Server) MethodOne(ctx context.Context, in *TestRequest) (*TestResponse, error) {
fmt.Printf("--- UnaryEcho ---\n")
md, ok := metadata.FromIncomingContext(ctx)
if !ok {
log.Println("miss metadata from context")
}
fmt.Println("md", md)
fmt.Printf("request received: %v, sending echo\n", in)
return &TestResponse{Message: in.Message}, nil
}
// 2.2流式响应接口实现
func (s *Server) ServerStreaming(in *TestRequest, stream MyGrpc_ServerStreamingServer) error {
fmt.Printf("--- ServerStreamingEcho ---\n")
fmt.Printf("request received: %v\n", in)
// Read requests and send responses.
for i := 0; i < 10; i++ {
fmt.Printf("echo message %v\n", in.Message)
//stream.Send()发送数据
err := stream.Send(&TestResponse{Message: in.Message})
if err != nil {
return err
}
}
return nil
}
// 2.3流式接收接口实现
func (s *Server) ClientStreaming(stream MyGrpc_ClientStreamingServer) error {
fmt.Printf("--- ClientStreamingEcho ---\n")
// Read requests and send responses.
var message string
for {
//stream.Recv()接收数据
in, err := stream.Recv()
if err == io.EOF {
fmt.Printf("echo last received message\n")
return stream.SendAndClose(&TestResponse{Message: message})
}
message = in.Message
fmt.Printf("request received: %v, building echo\n", in)
if err != nil {
return err
}
}
}
// 2.4双向数据流接口实现
func (s *Server) BidirectionalStreaming(stream MyGrpc_BidirectionalStreamingServer) error {
fmt.Printf("--- BidirectionalStreamingEcho ---\n")
// Read requests and send responses.
for {
//stream.Recv()接收数据
in, err := stream.Recv()
if err == io.EOF {
return nil
}
if err != nil {
return err
}
fmt.Printf("request received %v, sending echo\n", in)
//stream.Send()发送数据
if err := stream.Send(&TestResponse{Message: in.Message}); err != nil {
return err
}
}
}
// 不太清除这个接口是做什么的,假设不想生成这个接口可以执行 "protoc --go-grpc_out=require_unimplemented_servers=false" 关闭,
// 或者实现grpcServer的结构体中匿名嵌入UnimplementedXXXServer这个base结构体
func (s *Server) mustEmbedUnimplementedMyGrpcServer() {}
- main方法运行示例
// 3.main方法启动服务
func main() {
//1.监听tcp,指定端口
lis, err := net.Listen("tcp", ":8080")
if nil != err {
log.Fatalf("listen: %v", err)
}
//2.创建grpc服务器
s := grpc.NewServer()
//3.将服务实现注册到grpc服务器
RegisterMyGrpcServer(s, &Server{})
//4.监听启动服务
s.Serve(lis)
}
客户端代码示例
- 客户端根据业务需求,编写代码,调用生成的服务端接口,实现请求服务端指定接口业务
import (
"context"
"fmt"
"io"
"log"
"sync"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/metadata"
)
// 请求服务端普通接口示例
func MethodOneCallWithMetadata(c MyGrpcClient, message string) {
//1.设置请求头
md := metadata.Pairs("timestamp", time.Now().Format(time.StampNano))
md.Append("authorization", "Bearer AccessToken")
//2.创建ctx
ctx := metadata.NewOutgoingContext(context.Background(), md)
//3.设置请求参数,并调用生成的指定方法MethodOne()请求服务端
r, err := c.MethodOne(ctx, &TestRequest{Message: message})
if err != nil {
log.Fatalf("failed to call UnaryEcho: %v", err)
}
fmt.Printf("response:%v\n", r.Message)
}
// 请求服务端流式响应接口示例
func serverStreamingWithMetadata(c MyGrpcClient, message string) {
//1.设置请求头
md := metadata.Pairs("timestamp", time.Now().Format(time.StampNano))
md.Append("authorization", "Bearer AccessToken")
//2.创建ctx
ctx := metadata.NewOutgoingContext(context.Background(), md)
//3.服务端流式响应,调用生成的流式响应方法,构建流式响应stream
stream, err := c.ServerStreaming(ctx, &TestRequest{Message: message})
if err != nil {
log.Fatalf("failed to call ServerStreamingEcho: %v", err)
}
//4.遍历接收服务端响应
// Read all the responses.
var rpcStatus error
for {
//stream.Recv()接收流中的数据
r, err := stream.Recv()
if err != nil {
rpcStatus = err
break
}
fmt.Printf(" - %s\n", r.Message)
}
if rpcStatus != io.EOF {
log.Fatalf("failed to finish server streaming: %v", rpcStatus)
}
}
// 流式请求服务端接口示例
func clientStreamWithMetadata(c MyGrpcClient, message string) {
//1.设置请求头
md := metadata.Pairs("timestamp", time.Now().Format(time.StampNano))
md.Append("authorization", "Bearer AccessToken")
//2.创建ctx
ctx := metadata.NewOutgoingContext(context.Background(), md)
//3.客户端流式发送数据到服务端,调用生成的流式方法构建流式数据stream
stream, err := c.ClientStreaming(ctx)
if err != nil {
log.Fatalf("failed to call ClientStreamingEcho: %v\n", err)
}
//4.以流形式多次向服务端发送数据
for i := 0; i < 10; i++ {
//5.基于流式结构发送数据到服务端
if err := stream.Send(&TestRequest{Message: message}); err != nil {
log.Fatalf("failed to send streaming: %v\n", err)
}
}
//6.接收服务端响应并关闭连接
r, err := stream.CloseAndRecv()
if err != nil {
log.Fatalf("failed to CloseAndRecv: %v\n", err)
}
fmt.Printf("response:%v\n", r.Message)
}
// 双向数据流接口示例
func bidirectionalWithMetadata(c MyGrpcClient, message string) {
//1.设置请求头
md := metadata.Pairs("timestamp", time.Now().Format(time.StampNano))
md.Append("authorization", "Bearer AccessToken")
//2.创建ctx
ctx := metadata.NewOutgoingContext(context.Background(), md)
//3.客户端服务端双写流式通信,调用生成的方法创建双向流stream
stream, err := c.BidirectionalStreaming(ctx)
if err != nil {
log.Fatalf("failed to call BidirectionalStreamingEcho: %v\n", err)
}
//4.协程内部多次向服务端以流的形式发送数据
go func() {
// Send all requests to the server.
for i := 0; i < 10; i++ {
if err := stream.Send(&TestRequest{Message: message}); err != nil {
log.Fatalf("failed to send streaming: %v\n", err)
}
}
stream.CloseSend()
}()
//5..以流的形式接收服务端响应数据
var rpcStatus error
fmt.Printf("response:\n")
for {
r, err := stream.Recv()
if err != nil {
rpcStatus = err
break
}
fmt.Printf(" - %s\n", r.Message)
}
if rpcStatus != io.EOF {
log.Fatalf("failed to finish server streaming: %v", rpcStatus)
}
}
- main方法,创建grpc客户端,绑定服务端地址,调用该服务端生成的接口(也就是上方针对服务端生成的接口封装的业务接口)发起请求
func main() {
//遍历执行模拟并发
wg := sync.WaitGroup{}
for i := 0; i < 1; i++ {
wg.Add(1)
go func() {
defer wg.Done()
//1.与服务端建立连接
conn, err := grpc.Dial("localhost:8080", grpc.WithInsecure())
if err != nil {
log.Fatalf("did not connect: %v", err)
}
//退出时的关闭
defer conn.Close()
//2.调用生成的创建client方法,
//基于生成的Client创建
//grpcClient连接,并绑定连接
c := NewMyGrpcClient(conn)
//3.请求服务端一元方法(普通方法)
//for i := 0; i < 100; i++ {
MethodOneCallWithMetadata(c, message)
time.Sleep(400 * time.Millisecond)
//}
//
//4.请求服务端,流式响应方法
serverStreamingWithMetadata(c, message)
time.Sleep(1 * time.Second)
//5.流式请求服务端接口
clientStreamWithMetadata(c, message)
time.Sleep(1 * time.Second)
//6.双向流式
bidirectionalWithMetadata(c, message)
}()
}
wg.Wait()
time.Sleep(1 * time.Second)
}
五. grpc 拦截器
- grpc服务端和客户端都提供了interceptor功能,类似middleware,可以在拦截器中定义指定业务,例如验证处理,日志等
- Go-gRPC 实践指南: Interceptor 拦截器
六 gRpc底层通信的一些问题总结
- gRPC底层通信协议: gRPC底层使用的HTTP/2协议实现,查看请求响应头中的Content-Type会设置为 application/grpc
- 基于https建立连接时除了三次握手外还存在一次tls握手
- 连接建立后会发起一个连接前言请求(Magic帧 Setting帧),
- 通过Win_up滑动窗口传输数据
- 基于Header/Data帧进行传播和设置操作
- grpc会将数据存放到Data中, Data通常采用protobuf,需要设置IDL结构体,编解码数据进行传输
- 请求执行完毕后通过四次挥手断开连接
- gRPC 支持 4 种基础通信模式: 一元 RPC、服务器端流 RPC、客户端流 RPC 以及双向流 RPC
- 一元RPC模式: 在一元 RPC 模式中,gRPC 服务器端和 gRPC 客户端在通信时始终只有一个请求和一个响应。
- 服务器端流 RPC模式:
- 服务器端在接收到客户端的请求消息后,会发回一个响应的序列。这种多个响应所组成的序列也被称为“流”。在将所有的服务器端响应发送完毕之后,服务器端会以 trailer 元数据的形式将其状态发送给客户端,从而标记流的结束
- 客户端的 Go 语言实现中使用 Recv 方法从客户端流中检索消息,并且持续检索,直到流结束为止
- 客户端流RPC模式
- 客户端会发送多个请求给服务器端,而不再是单个请求,注意服务器端不一定要等到从客户端接收到所有消息后才发送响应,可以在接收到流中的一条消息或几条消息之后就发送响应,也可以在读取完流中的所有消息之后再发送响应
- 其实现原理是: 在grpc通信时,消息体以二进制形式一个或多个帧来发送消息数据,请求消息结束时会在最后一个数据帧上添加 END_STREAM 或发送一个带有 END_STREAM 空的数据帧也就是trailer,接收方会持续监测trailer,接收到后才认为结束