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)