trpc.group/trpc-go/trpc-go@v1.0.3/stream/README.zh_CN.md (about)

     1  [English](README.md) | 中文
     2  
     3  # tRPC-Go 搭建流式服务
     4  
     5  # 前言
     6  
     7  什么是流式:
     8  
     9  单次 RPC 需要客户端发起请求,等待服务端处理完毕,再返回给客户端。
    10  而流式 RPC 相比单次 RPC 而言,客户端和服务端建立流后可以持续不断发送数据,而服务端也可以持续不断接收数据,可以持续进行响应。
    11  
    12  tRPC 的流式,分为三种类型:
    13  
    14  - Server-side streaming RPC:服务端流式 RPC
    15  - Client-side streaming RPC:客户端流式 RPC
    16  - Bidirectional streaming RPC:双向流式 RPC
    17  
    18  流式为什么要存在呢,是 Simple RPC 有什么问题吗?使用 Simple RPC 时,有如下问题:
    19  
    20  - 数据包过大造成的瞬时压力
    21  - 接收数据包时,需要所有数据包都接受成功且正确后,才能够回调响应,进行业务处理(无法客户端边发送,服务端边处理)
    22  
    23  为什么用 Streaming RPC:
    24  
    25  - 大数据包,例如有一个大文件需要传输,如果使用 simple RPC,得自己分包,自己组合,解决不同包的乱序问题。使用流式可以客户端读出来后,直接传输,无需分包,无需关心乱序
    26  - 实时场景,比如多人聊天室,服务端接收到消息后,需要往多个客户端进行实时消息推送
    27  
    28  # 原理
    29  
    30  tRPC 流式设计原理见 [这里](https://github.com/trpc-group/trpc/blob/main/docs/cn/trpc_protocol_design.md)。
    31  
    32  # 示例
    33  
    34  ## 客户端流式
    35  
    36  ### 定义协议文件
    37  
    38  ```protobuf
    39  syntax = "proto3";
    40  
    41  package trpc.test.helloworld;
    42  option go_package="github.com/some-repo/examples/helloworld";
    43  
    44  // The greeting service definition.
    45  service Greeter {
    46    // Sends a greeting
    47    rpc SayHello (stream HelloRequest) returns (HelloReply);
    48  }
    49  // The request message containing the user's name.
    50  message HelloRequest {
    51    string name = 1;
    52  }
    53  // The response message containing the greetings
    54  message HelloReply {
    55    string message = 1;
    56  }
    57  ```
    58  
    59  ### 生成服务代码
    60  
    61  首先安装 [trpc-cmdline](https://github.com/trpc-group/trpc-cmdline)
    62  
    63  然后生成流式服务桩代码
    64  
    65  ```shell
    66  trpc create -p helloworld.proto
    67  ```
    68  
    69  ### 服务端代码
    70  
    71  ```go
    72  package main
    73  
    74  import (
    75      "fmt"
    76      "io"
    77      "strings"
    78      
    79      "trpc.group/trpc-go/trpc-go/log"
    80      trpc "trpc.group/trpc-go/trpc-go"
    81      _ "trpc.group/trpc-go/trpc-go/stream"
    82      pb "github.com/some-repo/examples/helloworld"
    83  )
    84  
    85  type greeterServerImpl struct{}
    86  
    87  // SayHello 客户端流式,SayHello 传入 pb.Greeter_SayHelloServer 作为参数,返回 error
    88  // pb.Greeter_SayHelloServer 提供 Recv() 和 SendAndClose() 等接口,用作流式交互 
    89  func (s *greeterServerImpl) SayHello(gs pb.Greeter_SayHelloServer) error {
    90      var names []string
    91      for {
    92          // 服务端使用 for 循环进行 Recv,接收来自客户的数据
    93          in, err := gs.Recv()
    94          if err == nil {
    95              log.Infof("receive hi, %s\n", in.Name)
    96          }
    97          // 如果返回 EOF,说明客户端流已经结束,客户端已经发送完所有数据
    98          if err == io.EOF {
    99              log.Infof("recveive error io eof %v\n", err)
   100              // SendAndClose 发送并关闭流
   101              gs.SendAndClose(&pb.HelloReply{Message: "hello " + strings.Join(names, ",")})
   102              return nil
   103          }
   104          // 说明流发生异常,需要返回
   105          if err != nil {
   106              log.Errorf("receive from %v\n", err)
   107              return err
   108          }
   109          names = append(names, in.Name)
   110      }
   111  }
   112  
   113  func main() {
   114      // 创建一个服务对象,底层会自动读取服务配置及初始化插件,必须放在 main 函数首行,业务初始化逻辑必须放在 NewServer 后面
   115      s := trpc.NewServer()
   116      // 注册当前实现到服务对象中
   117      pb.RegisterGreeterService(s, &greeterServerImpl{})
   118      // 启动服务,并阻塞在这里
   119      if err := s.Serve(); err != nil {
   120          panic(err)
   121      }
   122  }
   123  ```
   124  
   125  ### 客户端代码
   126  
   127  ```go
   128  package main
   129  
   130  import (
   131      "context"
   132      "flag"
   133      "fmt"
   134      "strconv"
   135      
   136      "trpc.group/trpc-go/trpc-go/client"
   137      "trpc.group/trpc-go/trpc-go/log"
   138      pb "github.com/some-repo/examples/helloworld"
   139  )
   140  
   141  func main() {
   142      target := flag.String("ipPort", "", "ip port")
   143      serviceName := flag.String("serviceName", "", "serviceName")
   144      
   145      flag.Parse()
   146      
   147      var ctx = context.Background()
   148      opts := []client.Option{
   149          client.WithNamespace("Development"),
   150          client.WithServiceName("trpc.test.helloworld.Greeter"),
   151          client.WithTarget(*target),
   152      }
   153      log.Debugf("client: %s,%s", *serviceName, *target)
   154      proxy := pb.NewGreeterClientProxy(opts...)
   155      // 有别于单次 RPC,调用 SayHello 不需要传入 request,返回 cstream 用于 send 和 recv
   156      cstream, err := proxy.SayHello(ctx, opts...)
   157      if err != nil {
   158          log.Error("Error in stream sayHello")
   159          return
   160      }
   161      for i := 0; i < 10; i++ {
   162          // 调用 Send 进行持续发送数据
   163          err = cstream.Send(&pb.HelloRequest{Name: "trpc-go" + strconv.Itoa(i)})
   164          if err != nil {
   165              log.Errorf("Send error %v\n", err)
   166              return err
   167          }
   168      }
   169      // 服务端只返回一次,所以调用 CloseAndRecv 进行接收
   170      reply, err := cstream.CloseAndRecv()
   171      if err == nil && reply != nil {
   172          log.Infof("reply is %s\n", reply.Message)
   173          
   174      }
   175      if err != nil {
   176          log.Errorf("receive error from server :%v", err)
   177      }
   178  }
   179  ```
   180  
   181  ## 服务端流式
   182  
   183  ### 定义协议文件
   184  
   185  ```protobuf
   186  service Greeter {
   187    // HelloReply 前面加 stream
   188    rpc SayHello (HelloRequest) returns (stream HelloReply);
   189  }
   190  ```
   191  
   192  ### 服务端代码
   193  
   194  ```go
   195  // SayHello 服务端流式,SayHello 传入一次 request 和 pb.Greeter_SayHelloServer 作为参数,返回 error
   196  // pb.Greeter_SayHelloServer 提供 Send() 接口,用作流式交互 
   197  func (s *greeterServerImpl) SayHello(in *pb.HelloRequest, gs pb.Greeter_SayHelloServer) error {
   198      name := in.Name
   199      for i := 0; i < 100; i++ {
   200          // 持续调用 Send 进行发送响应
   201          gs.Send(&pb.HelloReply{Message: "hello " + name + strconv.Itoa(i)})
   202      }
   203      return nil
   204  }
   205  
   206  ```
   207  
   208  ### 客户端代码
   209  
   210  ```go
   211  func main() {
   212      proxy := pb.NewGreeterClientProxy(opts...)
   213      // 客户端直接填入参数,返回 cstream 可以用来持续接收服务端相应
   214      cstream, err := proxy.SayHello(ctx, &pb.HelloRequest{Name: "trpc-go"}, opts...)
   215      if err != nil {
   216          log.Error("Error in stream sayHello")
   217          return
   218      }
   219      for {
   220          reply, err := cstream.Recv()
   221          // 注意这里不能使用 errors.Is(err, io.EOF) 来判断流结束
   222          if err == io.EOF {
   223              break
   224          }
   225          if err != nil {
   226              log.Infof("failed to recv: %v\n", err)
   227          }
   228          log.Infof("Greeting:%s \n", reply.Message)
   229      }
   230  }
   231  ```
   232  
   233  ## 双向流式
   234  
   235  ### 定义协议文件
   236  
   237  ```protobuf
   238  service Greeter {
   239    rpc SayHello (stream HelloRequest) returns (stream HelloReply) {}
   240  }
   241  ```
   242  
   243  ### 服务端代码
   244  
   245  ```go
   246  // SayHello 双向流式,SayHello 传入 pb.Greeter_SayHelloServer 作为参数,返回 error
   247  // pb.Greeter_SayHelloServer 提供 Recv() 和 Send() 接口,用作流式交互 
   248  func (s *greeterServerImpl) SayHello(gs pb.Greeter_SayHelloServer) error {
   249      var names []string
   250      for {
   251          // 循环调用 Recv
   252          in, err := gs.Recv()
   253          if err == nil {
   254              log.Infof("receive hi, %s\n", in.Name)
   255          }
   256          if err == io.EOF {
   257              log.Infof("recveive error io eof %v\n", err)
   258              // EOF 代表客户端流消息已经发送结束,
   259              gs.Send(&pb.HelloReply{Message: "hello " + strings.Join(names, ",")})
   260              return nil
   261          }
   262          if err != nil {
   263              log.Errorf("receive from %v\n", err)
   264              return err
   265          }
   266          names = append(names, in.Name)
   267      }
   268  }
   269  ```
   270  
   271  ### 客户端代码
   272  
   273  ```go
   274  func main() {
   275      proxy := pb.NewGreeterClientProxy(opts...)
   276      cstream, err := proxy.SayHello(ctx, opts...)
   277      if err != nil {
   278          log.Error("Error in stream sayHello %v", err)
   279          return
   280      }
   281      for i := 0; i < 10; i++ {
   282          // 持续发送消息
   283          cstream.Send(&pb.HelloRequest{Name: "jesse" + strconv.Itoa(i)})
   284      }
   285      // 调用 CloseSend 代表流已经结束
   286      err = cstream.CloseSend()
   287      if err != nil {
   288          log.Infof("error is %v \n", err)
   289          return
   290      }
   291      for {
   292          // 持续调用 Recv,接收服务端响应
   293          reply, err := cstream.Recv()
   294          if err == nil && reply != nil {
   295              log.Infof("reply is %s\n", reply.Message)
   296          }
   297          // 注意这里不能使用 errors.Is(err, io.EOF) 来判断流结束
   298          if err == io.EOF {
   299              log.Infof("recvice EOF: %v\n", err)
   300              break
   301          }
   302          if err != nil {
   303              log.Errorf("receive error from server :%v", err)
   304          }
   305      }
   306      if err != nil {
   307          log.Fatal(err)
   308      }
   309  }
   310  ```
   311  
   312  # 流控
   313  
   314  如果发送方发送速度过快,接收方来不及处理怎么办?可能会导致接收方过载,内存超限等等
   315  为了解决这个问题,tRPC 实现了和 http2.0 类似的流控功能
   316  
   317  - tRPC 的流控针对单个流,不对整个连接进行流量控制
   318  - 和 HTTP2.0 一样,整个 flow control 基于对发送方的信任
   319  - tRPC 发送端可以设置初始的发送窗口大小(针对单个流),在 tRPC 流式初始化过程中,将这个窗口大小通告给接收方
   320  - 接收方接受到初始窗口大小之后,记录在本地,发送端每发送一个 DATA 帧,就把这个发送窗口值减去 Data 帧有效数据的大小(payload,不包括帧头)
   321  - 如果递减过程,如果当前可用窗口小于 0,那么将不能发送,这里不进行帧的拆分(http2.0 进行拆分),上层 API 进行阻塞
   322  - 接收端每消费 1/4 的初始窗口大小进行 feedback,发送一个 feedback 帧,携带增量的 window size,发送端接收到这个增量 window size 之后加到本地可发送的 window 大小
   323  - 帧分优先级,对于 feedback 的帧不做流控,优先级高于 Data 帧,防止因为优先级问题导致 feedback 帧发生阻塞
   324  
   325  tRPC-Go 默认启用流控,目前默认窗口大小为 65535,如果连续发送超过 65535 大小的数据(序列化和压缩后),接收方没调用 Recv,则发送方会 block
   326  如果要设置客户端接收窗口大小,使用 client option `WithMaxWindowSize`
   327  
   328  ```go
   329  opts := []client.Option{
   330      client.WithNamespace("Development"),
   331      client.WithMaxWindowSize(1 * 1024 * 1024),
   332      client.WithServiceName("trpc.test.helloworld.Greeter"),
   333      client.WithTarget(*target),
   334  }
   335  proxy := pb.NewGreeterClientProxy(opts...)
   336  ...
   337  ```
   338  
   339  如果要设置服务端接收窗口大小,使用 server option `WithMaxWindowSize`
   340  
   341  ```go
   342  s := trpc.NewServer(server.WithMaxWindowSize(1 * 1024 * 1024))
   343  pb.RegisterGreeterService(s, &greeterServiceImpl{})
   344  if err := s.Serve(); err != nil {
   345      log.Fatal(err)
   346  }
   347  ```
   348  
   349  # 注意事项
   350  
   351  ## 流式服务只支持同步模式
   352  
   353  当 pb 里面同一个 service 既定义有普通 rpc 方法 和 流式方法时,用户自行设置启用异步模式会失效,只能使用同步模式。原因是流式只支持同步模式,所以如果想要使用异步模式的话,就必须定义一个只有普通 rpc 方法的 service。
   354  
   355  ## 流式客户端判断流结束必须使用 `err == io.EOF`
   356  
   357  判断流结束应该明确用 `err == io.EOF`,而不是 `errors.Is(err, io.EOF)`,因为底层连接断开可能返回 `io.EOF`,框架对其封装后返回给业务层,业务判断时出现 `errors.Is(err, io.EOF) == true`,这个时候可能会误认为流被正常关闭了,实际上是底层连接断开,流是非正常结束的。
   358  
   359  # 拦截器
   360  
   361  流式拦截器见 [trpc-go/filter](/filter)