trpc.group/trpc-go/trpc-go@v1.0.3/http/README.zh_CN.md (about) 1 [English](README.md) | 中文 2 3 # tRPC-Go HTTP 协议 4 5 tRPC-Go 框架支持搭建与 HTTP 相关的三种服务: 6 7 1. 泛 HTTP 标准服务 (无需桩代码及 IDL 文件) 8 2. 泛 HTTP RPC 服务 (共享 RPC 协议使用的桩代码以及 IDL 文件) 9 3. 泛 HTTP RESTful 服务 (基于 IDL 及桩代码提供 RESTful API) 10 11 其中 RESTful 相关文档见 [/restful](/restful/) 12 13 ## 泛 HTTP 标准服务 14 15 tRPC-Go 框架提供了泛 HTTP 标准服务能力, 主要是在标准库 HTTP 的能力上添加了服务注册、服务发现、拦截器等能力, 使 HTTP 协议能够无缝接入 tRPC 生态 16 17 相较于 tRPC 协议而言, 泛 HTTP 标准服务服务不依赖桩代码, 因此服务侧对应的 protocol 名为 `http_no_protocol` 18 19 ### 服务端 20 21 #### 配置编写 22 23 在 `trpc_go.yaml` 配置文件中配置 service,协议为 `http_no_protocol`,http2 则为 `http2_no_protocol`: 24 25 ```yaml 26 server: 27 service: # 业务服务提供的 service,可以有多个 28 - name: trpc.app.server.stdhttp # service 的路由名称 29 network: tcp # 网络监听类型,tcp 或 udp 30 protocol: http_no_protocol # 应用层协议 http_no_protocol 31 timeout: 1000 # 请求最长处理时间,单位毫秒 32 ip: 127.0.0.1 33 port: 8080 # 服务监听端口 34 ``` 35 36 注意确保配置文件的正常加载 37 38 #### 代码编写 39 40 ##### 单一 URL 注册 41 42 ```go 43 import ( 44 "net/http" 45 46 "trpc.group/trpc-go/trpc-go/codec" 47 "trpc.group/trpc-go/trpc-go/log" 48 thttp "trpc.group/trpc-go/trpc-go/http" 49 trpc "trpc.group/trpc-go/trpc-go" 50 ) 51 52 func main() { 53 s := trpc.NewServer() 54 thttp.HandleFunc("/xxx", handle) 55 // 注册 NoProtocolService 时传的参数必须和配置中的 service name 一致: s.Service("trpc.app.server.stdhttp") 56 thttp.RegisterNoProtocolService(s.Service("trpc.app.server.stdhttp")) 57 s.Serve() 58 } 59 60 func handle(w http.ResponseWriter, r *http.Request) error { 61 // handle 的编写方法完全同标准库 HTTP 的使用方式一致 62 // 比如可以在 r 中读取 Header 等 63 // 可以在 r.Body 对 client 进行流式读包 64 clientReq, err := io.ReadAll(r.Body) 65 if err != nil { /*..*/ } 66 // 最后使用 w 来进行回包 67 w.Header().Set("Content-type", "application/text") 68 w.WriteHeader(http.StatusOK) 69 w.Write([]byte("response body")) 70 return nil 71 } 72 ``` 73 74 ##### MUX 注册 75 76 ```go 77 import ( 78 "net/http" 79 80 "trpc.group/trpc-go/trpc-go/codec" 81 "trpc.group/trpc-go/trpc-go/log" 82 thttp "trpc.group/trpc-go/trpc-go/http" 83 trpc "trpc.group/trpc-go/trpc-go" 84 "github.com/gorilla/mux" 85 ) 86 87 func main() { 88 s := trpc.NewServer() 89 // 路由注册 90 router := mux.NewRouter() 91 router.HandleFunc("/{dir0}/{dir1}/{day}/{hour}/{vid:[a-z0-9A-Z]+}_{index:[0-9]+}.jpg", handle). 92 Methods("GET") 93 // 注册 RegisterNoProtocolServiceMux 时传的参数必须和配置中的 service name 一致: s.Service("trpc.app.server.stdhttp") 94 thttp.RegisterNoProtocolServiceMux(s.Service("trpc.app.server.stdhttp"), router) 95 s.Serve() 96 } 97 98 func handle(w http.ResponseWriter, r *http.Request) error { 99 // 取 url 中的参数 100 vars := mux.Vars(r) 101 vid := vars["vid"] 102 index := vars["index"] 103 log.Infof("vid: %s, index: %s", vid, index) 104 return nil 105 } 106 ``` 107 108 ### 客户端 109 110 这里指的是调用一个标准 HTTP 服务, 下游这个标准 HTTP 服务并不一定是基于 tRPC-Go 框架构建的 111 112 最简洁的方式实际上是直接使用标准库提供的 HTTP Client, 但是就无法使用服务发现以及各种插件拦截器提供的能力(比如监控上报) 113 114 #### 配置编写 115 116 ```yaml 117 client: # 客户端调用的后端配置 118 timeout: 1000 # 针对所有后端的请求最长处理时间 119 namespace: Development # 针对所有后端的环境 120 filter: # 针对所有后端调用函数前后的拦截器列表 121 - simpledebuglog # 这是 debug log 拦截器, 可以再添加其他拦截器, 比如监控等 122 service: # 针对单个后端的配置 123 - name: trpc.app.server.stdhttp # 下游 http 服务的 service name 124 ## 可以使用 target 来选用其他的 selector, 只有 service name 的情况下默认会使用北极星做服务发现(在使用了北极星插件的情况下) 125 # target: polaris://trpc.app.server.stdhttp # 或者 ip://127.0.0.1:8080 来指定 ip:port 进行调用 126 ``` 127 128 #### 代码编写 129 130 ```go 131 package main 132 133 import ( 134 "context" 135 136 trpc "trpc.group/trpc-go/trpc-go" 137 "trpc.group/trpc-go/trpc-go/client" 138 "trpc.group/trpc-go/trpc-go/codec" 139 trpc "trpc.group/trpc-go/trpc-go" 140 "trpc.group/trpc-go/trpc-go/log" 141 ) 142 143 // Data 请求报文数据 144 type Data struct { 145 Msg string 146 } 147 148 func main() { 149 // 省略掉 tRPC-Go 框架配置加载部分, 假如以下逻辑在某个 RPC handle 中, 配置一般已经正常加载 150 // 创建 ClientProxy, 设置协议为 HTTP 协议,序列化为 JSON 151 httpCli := http.NewClientProxy("trpc.app.server.stdhttp", 152 client.WithSerializationType(codec.SerializationTypeJSON)) 153 reqHeader := &http.ClientReqHeader{} 154 // 为 HTTP Head 添加 request 字段 155 reqHeader.AddHeader("request", "test") 156 rspHead := &http.ClientRspHeader{} 157 req := &Data{Msg: "Hello, I am stdhttp client!"} 158 rsp := &Data{} 159 // 发送 HTTP POST 请求 160 if err := httpCli.Post(context.Background(), "/v1/hello", req, rsp, 161 client.WithReqHead(reqHeader), 162 client.WithRspHead(rspHead), 163 ); err != nil { 164 log.Warn("get http response err") 165 return 166 } 167 // 获取 HTTP 响应报文头中的 reply 字段 168 replyHead := rspHead.Response.Header.Get("reply") 169 log.Infof("data is %s, request head is %s\n", rsp, replyHead) 170 } 171 ``` 172 173 174 ## 泛 HTTP RPC 服务 175 176 相较于**泛 HTTP 标准服务**, 泛 HTTP RPC 服务的最大区别是复用了 IDL 协议文件及其生成的桩代码, 同时无缝融入了 tRPC 生态(服务注册、服务路由、服务发现、各种插件拦截器等) 177 178 注意: 179 180 在这种服务形式下, HTTP 协议与 tRPC 协议保持一致:当服务端返回失败时,body 为空,错误码错误信息放在 HTTP header 里 181 182 ### 服务端 183 184 #### 配置编写 185 186 首先需要生成桩代码: 187 188 ```shell 189 trpc create -p helloworld.proto --protocol http -o out 190 ``` 191 192 假如本身已经是一个 tRPC 服务已经存在桩代码, 只是想在同样的接口上支持 HTTP 协议, 那么无需再次生成桩代码, 而是在配置中添加 `http` 协议项即可 193 194 ```yaml 195 server: # 服务端配置 196 service: 197 ## 同一套接口可以通过两份配置同时提供 trpc 协议以及 http 协议服务 198 - name: trpc.test.helloworld.Greeter # service 的路由名称 199 ip: 127.0.0.0 # 服务监听 ip 地址 可使用占位符 ${ip},ip 和 nic 二选一,优先 ip 200 port: 80 # 服务监听端口 可使用占位符 ${port} 201 protocol: trpc # 应用层协议 trpc http 202 ## 以下为主要示例, 注意应用层协议为 http 203 - name: trpc.test.helloworld.GreeterHTTP # service 的路由名称 204 ip: 127.0.0.0 # 服务监听 ip 地址 可使用占位符 ${ip},ip 和 nic 二选一,优先 ip 205 port: 80 # 服务监听端口 可使用占位符 ${port} 206 protocol: http # 应用层协议 trpc http 207 ``` 208 209 #### 代码编写 210 211 ```go 212 import ( 213 "context" 214 "fmt" 215 216 trpc "trpc.group/trpc-go/trpc-go" 217 "trpc.group/trpc-go/trpc-go/client" 218 pb "github.com/xxxx/helloworld/pb" 219 ) 220 221 func main() { 222 s := trpc.NewServer() 223 hello := Hello{} 224 pb.RegisterHelloTrpcGoService(s.Service("trpc.test.helloworld.Greeter"), &hello) 225 // 和一般的 tRPC 服务注册是一致的 226 pb.RegisterHelloTrpcGoService(s.Service("trpc.test.helloworld.GreeterHTTP"), &hello) 227 log.Println(s.Serve()) 228 } 229 230 type Hello struct {} 231 232 // RPC 服务接口的实现无需感知 HTTP 协议, 只需按照通常的逻辑处理请求并返回响应即可 233 func (h *Hello) Hello(ctx context.Context, req *pb.HelloReq) (*pb.HelloRsp, error) { 234 fmt.Println("--- got HelloReq", req) 235 time.Sleep(time.Second) 236 return &pb.HelloRsp{Msg: "Welcome " + req.Name}, nil 237 } 238 ``` 239 #### 自定义 URL path 240 241 默认为 `/package.service/method`,可通过 alias 参数自定义任意 URL 242 243 - 协议定义: 244 245 ```protobuf 246 syntax = "proto3"; 247 package trpc.app.server; 248 option go_package="github.com/your_repo/app/server"; 249 250 import "trpc.proto"; 251 252 message Request { 253 bytes req = 1; 254 } 255 256 message Reply { 257 bytes rsp = 1; 258 } 259 260 service Greeter { 261 rpc SayHello(Request) returns (Reply) { 262 option (trpc.alias) = "/cgi-bin/module/say_hello"; 263 }; 264 } 265 ``` 266 267 #### 自定义错误码处理函数 268 269 默认错误码处理函数,会将错误码填充到 HTTP header 的 `trpc-ret/trpc-func-ret` 字段中,也可以通过自己定义 ErrorHandler 进行替换。 270 271 ```golang 272 import ( 273 "net/http" 274 275 "trpc.group/trpc-go/trpc-go/errs" 276 thttp "trpc.group/trpc-go/trpc-go/http" 277 ) 278 279 func init() { 280 thttp.DefaultServerCodec.ErrHandler = func(w http.ResponseWriter, r *http.Request, e *errs.Error) { 281 // 一般自行定义 retcode retmsg 字段,组成 json 并写到 response body 里 282 w.Write([]byte(fmt.Sprintf(`{"retcode":%d, "retmsg":"%s"}`, e.Code, e.Msg))) 283 // 每个业务团队可以定义到自己的 git 里,业务代码 import 进来即可 284 } 285 } 286 ``` 287 288 ### 客户端 289 290 #### 配置编写 291 292 和一般的 RPC Client 书写方式相同, 只需把配置 `protocol` 改为 `http`: 293 294 ```yaml 295 client: 296 namespace: Development # 针对所有后端的环境 297 filter: # 针对所有后端调用函数前后的拦截器列表 298 service: # 针对单个后端的配置 299 - name: trpc.test.helloworld.GreeterHTTP # 后端服务的 service name 300 network: tcp # 后端服务的网络类型 tcp udp 301 protocol: http # 应用层协议 trpc http 302 ## 可以使用 target 来选用其他的 selector, 只有 service name 的情况下默认会使用北极星做服务发现(在使用了北极星插件的情况下) 303 # target: ip://127.0.0.1:8000 # 请求服务地址 304 timeout: 1000 # 请求最长处理时间 305 ``` 306 307 #### 代码编写 308 309 ```go 310 import ( 311 "context" 312 "net/http" 313 314 "trpc.group/trpc-go/trpc-go/client" 315 thttp "trpc.group/trpc-go/trpc-go/http" 316 "trpc.group/trpc-go/trpc-go/log" 317 pb "trpc.group/trpc-go/trpc-go/testdata/trpc/helloworld" 318 ) 319 320 func main() { 321 // 省略掉 tRPC-Go 框架配置加载部分, 假如以下逻辑在某个 RPC handle 中, 配置一般已经正常加载 322 // 创建 ClientProxy, 设置协议为 HTTP 协议, 序列化为 JSON 323 proxy := pb.NewGreeterClientProxy() 324 reqHeader := &thttp.ClientReqHeader{} 325 // 必须留空或设置为 "POST" 326 reqHeader.Method = "POST" 327 // 为 HTTP Head 添加 request 字段 328 reqHeader.AddHeader("request", "test") 329 // 设置 Cookie 330 cookie := &http.Cookie{Name: "sample", Value: "sample", HttpOnly: false} 331 reqHeader.AddHeader("Cookie", cookie.String()) 332 req := &pb.HelloRequest{Msg: "Hello, I am tRPC-Go client."} 333 rspHead := &thttp.ClientRspHeader{} 334 // 发送 HTTP RPC 请求 335 rsp, err := proxy.SayHello(context.Background(), req, 336 client.WithReqHead(reqHeader), 337 client.WithRspHead(rspHead), 338 // 此处可以使用代码强制覆盖 trpc_go.yaml 配置中的 target 字段来设置其他 selector, 一般没必要, 这里只是展示有这个功能 339 // client.WithTarget("ip://127.0.0.1:8000"), 340 ) 341 if err != nil { 342 log.Warn("get http response err") 343 return 344 } 345 // 获取 HTTP 响应报文头中的 reply 字段 346 replyHead := rspHead.Response.Header.Get("reply") 347 log.Infof("data is %s, request head is %s\n", rsp, replyHead) 348 } 349 ``` 350 351 ## FAQ 352 353 ### 客户端及服务端开启 HTTPS 354 355 #### 双向认证 356 357 ##### 仅配置填写 358 359 只需在 `trpc_go.yaml` 中添加相应的配置项(证书以及私钥): 360 361 ```yaml 362 server: # 服务端配置 363 service: # 业务服务提供的 service,可以有多个 364 - name: trpc.app.server.stdhttp 365 network: tcp 366 protocol: http_no_protocol # 泛 HTTP RPC 服务则填 http 367 tls_cert: "../testdata/server.crt" # 添加证书路径 368 tls_key: "../testdata/server.key" # 添加私钥路径 369 ca_cert: "../testdata/ca.pem" # CA 证书, 需要双向认证时可填写 370 client: # 客户端配置 371 service: # 业务服务提供的 service,可以有多个 372 - name: trpc.app.server.stdhttp 373 network: tcp 374 protocol: http 375 tls_cert: "../testdata/server.crt" # 添加证书路径 376 tls_key: "../testdata/server.key" # 添加私钥路径 377 ca_cert: "../testdata/ca.pem" # CA 证书, 需要双向认证时可填写 378 ``` 379 380 代码中不在需要额外考虑任何和 TLS/HTTPS 相关的操作(不需要指定 scheme 为 `https`, 不需要手动添加 `WithTLS` option, 也不需要在 `WithTarget` 等其他地方想办法塞一个有关 HTTPS 的标识进去) 381 382 ##### 仅代码填写 383 384 服务端使用 `server.WithTLS` 依次指定服务端证书、私钥、CA 证书即可: 385 386 ```go 387 server.WithTLS( 388 "../testdata/server.crt", 389 "../testdata/server.key", 390 "../testdata/ca.pem", 391 ), 392 ``` 393 394 客户端使用 `client.WithTLS` 依次指定客户端端证书、私钥、CA 证书即可: 395 396 ```go 397 client.WithTLS( 398 "../testdata/client.crt", 399 "../testdata/client.key", 400 "../testdata/ca.pem", 401 "localhost", // 填写 server name 402 ), 403 ``` 404 405 除了这两个 option 以外, 代码中不在需要额外考虑任何和 TLS/HTTPS 相关的操作 406 407 示例如下: 408 409 ```go 410 func TestHTTPSUseClientVerify(t *testing.T) { 411 const ( 412 network = "tcp" 413 address = "127.0.0.1:0" 414 ) 415 ln, err := net.Listen(network, address) 416 require.Nil(t, err) 417 defer ln.Close() 418 serviceName := "trpc.app.server.Service" + t.Name() 419 service := server.New( 420 server.WithServiceName(serviceName), 421 server.WithNetwork("tcp"), 422 server.WithProtocol("http_no_protocol"), 423 server.WithListener(ln), 424 server.WithTLS( 425 "../testdata/server.crt", 426 "../testdata/server.key", 427 "../testdata/ca.pem", 428 ), 429 ) 430 thttp.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) error { 431 w.Write([]byte(t.Name())) 432 return nil 433 }) 434 thttp.RegisterNoProtocolService(service) 435 s := &server.Server{} 436 s.AddService(serviceName, service) 437 go s.Serve() 438 defer s.Close(nil) 439 time.Sleep(100 * time.Millisecond) 440 441 c := thttp.NewClientProxy( 442 serviceName, 443 client.WithTarget("ip://"+ln.Addr().String()), 444 ) 445 req := &codec.Body{} 446 rsp := &codec.Body{} 447 require.Nil(t, 448 c.Post(context.Background(), "/", req, rsp, 449 client.WithCurrentSerializationType(codec.SerializationTypeNoop), 450 client.WithSerializationType(codec.SerializationTypeNoop), 451 client.WithCurrentCompressType(codec.CompressTypeNoop), 452 client.WithTLS( 453 "../testdata/client.crt", 454 "../testdata/client.key", 455 "../testdata/ca.pem", 456 "localhost", 457 ), 458 )) 459 require.Equal(t, []byte(t.Name()), rsp.Data) 460 } 461 ``` 462 463 #### 不认证客户端证书 464 465 ##### 仅配置填写 466 467 只需在 `trpc_go.yaml` 中添加相应的配置项(证书以及私钥): 468 469 ```yaml 470 server: # 服务端配置 471 service: # 业务服务提供的 service,可以有多个 472 - name: trpc.app.server.stdhttp 473 network: tcp 474 protocol: http_no_protocol # 泛 HTTP RPC 服务则填 http 475 tls_cert: "../testdata/server.crt" # 添加证书路径 476 tls_key: "../testdata/server.key" # 添加私钥路径 477 # ca_cert: "" # CA 证书, 不认证客户端证书时此处不填或留空 478 client: # 客户端配置 479 service: # 业务服务提供的 service,可以有多个 480 - name: trpc.app.server.stdhttp 481 network: tcp 482 protocol: http 483 # tls_cert: "" # 证书路径, 不认证客户端证书时此处不填或留空 484 # tls_key: "" # 私钥路径, 不认证客户端证书时此处不填或留空 485 ca_cert: "none" # CA 证书, 不认证客户端证书时此处必须填写, 并且要填 "none" 486 ``` 487 488 可以双向认证部分, 主要的区别在于服务端的 `ca_cert` 需要留空, 客户端的 `ca_cert` 需要填 `none` 489 490 代码中不在需要额外考虑任何和 TLS/HTTPS 相关的操作(不需要指定 scheme 为 `https`, 不需要手动添加 `WithTLS` option, 也不需要在 `WithTarget` 等其他地方想办法塞一个有关 HTTPS 的标识进去) 491 492 ##### 仅代码填写 493 494 服务端使用 `server.WithTLS` 依次指定服务端证书、私钥、CA 证书即可: 495 496 ```go 497 server.WithTLS( 498 "../testdata/server.crt", 499 "../testdata/server.key", 500 "", // CA 证书, 不认证客户端证书时此处留空 501 ), 502 ``` 503 504 客户端使用 `client.WithTLS` 依次指定客户端端证书、私钥、CA 证书即可: 505 506 ```go 507 client.WithTLS( 508 "", // 证书路径, 留空 509 "", // 私钥路径, 留空 510 "none", // CA 证书, 不认证客户端证书时此处必须填 "none" 511 "", // server name, 留空 512 ), 513 ``` 514 515 除了这两个 option 以外, 代码中不在需要额外考虑任何和 TLS/HTTPS 相关的操作 516 517 示例如下: 518 519 520 ```go 521 func TestHTTPSSkipClientVerify(t *testing.T) { 522 const ( 523 network = "tcp" 524 address = "127.0.0.1:0" 525 ) 526 ln, err := net.Listen(network, address) 527 require.Nil(t, err) 528 defer ln.Close() 529 serviceName := "trpc.app.server.Service" + t.Name() 530 service := server.New( 531 server.WithServiceName(serviceName), 532 server.WithNetwork("tcp"), 533 server.WithProtocol("http_no_protocol"), 534 server.WithListener(ln), 535 server.WithTLS( 536 "../testdata/server.crt", 537 "../testdata/server.key", 538 "", 539 ), 540 ) 541 thttp.HandleFunc("/", func(w http.ResponseWriter, _ *http.Request) error { 542 w.Write([]byte(t.Name())) 543 return nil 544 }) 545 thttp.RegisterNoProtocolService(service) 546 s := &server.Server{} 547 s.AddService(serviceName, service) 548 go s.Serve() 549 defer s.Close(nil) 550 time.Sleep(100 * time.Millisecond) 551 552 c := thttp.NewClientProxy( 553 serviceName, 554 client.WithTarget("ip://"+ln.Addr().String()), 555 ) 556 req := &codec.Body{} 557 rsp := &codec.Body{} 558 require.Nil(t, 559 c.Post(context.Background(), "/", req, rsp, 560 client.WithCurrentSerializationType(codec.SerializationTypeNoop), 561 client.WithSerializationType(codec.SerializationTypeNoop), 562 client.WithCurrentCompressType(codec.CompressTypeNoop), 563 client.WithTLS( 564 "", "", "none", "", 565 ), 566 )) 567 require.Equal(t, []byte(t.Name()), rsp.Data) 568 } 569 ``` 570 571 572 ### 客户端使用 io.Reader 进行流式发送文件 573 574 需要 trpc-go 版本 >= v0.13.0 575 576 关键点在于将一个 `io.Reader` 填到 `thttp.ClientReqHeader.ReqBody` 字段上 (`body` 是一个 `io.Reader`): 577 578 ```go 579 reqHeader := &thttp.ClientReqHeader{ 580 Header: header, 581 ReqBody: body, // Stream send. 582 } 583 ``` 584 585 然后在调用时指定 `client.WithReqHead(reqHeader)`: 586 587 ```go 588 c.Post(context.Background(), "/", req, rsp, 589 client.WithCurrentSerializationType(codec.SerializationTypeNoop), 590 client.WithSerializationType(codec.SerializationTypeNoop), 591 client.WithCurrentCompressType(codec.CompressTypeNoop), 592 client.WithReqHead(reqHeader), 593 ) 594 ``` 595 596 示例如下: 597 598 ```go 599 func TestHTTPStreamFileUpload(t *testing.T) { 600 // Start server. 601 const ( 602 network = "tcp" 603 address = "127.0.0.1:0" 604 ) 605 ln, err := net.Listen(network, address) 606 require.Nil(t, err) 607 defer ln.Close() 608 go http.Serve(ln, &fileHandler{}) 609 // Start client. 610 c := thttp.NewClientProxy( 611 "trpc.app.server.Service_http", 612 client.WithTarget("ip://"+ln.Addr().String()), 613 ) 614 // Open and read file. 615 fileDir, err := os.Getwd() 616 require.Nil(t, err) 617 fileName := "README.md" 618 filePath := path.Join(fileDir, fileName) 619 file, err := os.Open(filePath) 620 require.Nil(t, err) 621 defer file.Close() 622 // Construct multipart form file. 623 body := &bytes.Buffer{} 624 writer := multipart.NewWriter(body) 625 part, err := writer.CreateFormFile("field_name", filepath.Base(file.Name())) 626 require.Nil(t, err) 627 io.Copy(part, file) 628 require.Nil(t, writer.Close()) 629 // Add multipart form data header. 630 header := http.Header{} 631 header.Add("Content-Type", writer.FormDataContentType()) 632 reqHeader := &thttp.ClientReqHeader{ 633 Header: header, 634 ReqBody: body, // Stream send. 635 } 636 req := &codec.Body{} 637 rsp := &codec.Body{} 638 // Upload file. 639 require.Nil(t, 640 c.Post(context.Background(), "/", req, rsp, 641 client.WithCurrentSerializationType(codec.SerializationTypeNoop), 642 client.WithSerializationType(codec.SerializationTypeNoop), 643 client.WithCurrentCompressType(codec.CompressTypeNoop), 644 client.WithReqHead(reqHeader), 645 )) 646 require.Equal(t, []byte(fileName), rsp.Data) 647 } 648 649 type fileHandler struct{} 650 651 func (*fileHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 652 _, h, err := r.FormFile("field_name") 653 if err != nil { 654 w.WriteHeader(http.StatusBadRequest) 655 return 656 } 657 w.WriteHeader(http.StatusOK) 658 // Write back file name. 659 w.Write([]byte(h.Filename)) 660 return 661 } 662 ``` 663 664 ### 客户端使用 io.Reader 进行流式读取回包 665 666 需要 trpc-go 版本 >= v0.13.0 667 668 关键在于添加 `thttp.ClientRspHeader` 并指定 `thttp.ClientRspHeader.ManualReadBody` 字段为 `true`: 669 670 ```go 671 rspHead := &thttp.ClientRspHeader{ 672 ManualReadBody: true, 673 } 674 ``` 675 676 然后调用时加上 `client.WithRspHead(rspHead)`: 677 678 ```go 679 c.Post(context.Background(), "/", req, rsp, 680 client.WithCurrentSerializationType(codec.SerializationTypeNoop), 681 client.WithSerializationType(codec.SerializationTypeNoop), 682 client.WithCurrentCompressType(codec.CompressTypeNoop), 683 client.WithRspHead(rspHead), 684 ) 685 ``` 686 687 最后可以在 `rspHead.Response.Body` 上进行流式读包: 688 689 ```go 690 body := rspHead.Response.Body // Do stream reads directly from rspHead.Response.Body. 691 defer body.Close() // Do remember to close the body. 692 bs, err := io.ReadAll(body) 693 ``` 694 695 示例如下: 696 697 ```go 698 func TestHTTPStreamRead(t *testing.T) { 699 // Start server. 700 const ( 701 network = "tcp" 702 address = "127.0.0.1:0" 703 ) 704 ln, err := net.Listen(network, address) 705 require.Nil(t, err) 706 defer ln.Close() 707 go http.Serve(ln, &fileServer{}) 708 709 // Start client. 710 c := thttp.NewClientProxy( 711 "trpc.app.server.Service_http", 712 client.WithTarget("ip://"+ln.Addr().String()), 713 ) 714 715 // Enable manual body reading in order to 716 // disable the framework's automatic body reading capability, 717 // so that users can manually do their own client-side streaming reads. 718 rspHead := &thttp.ClientRspHeader{ 719 ManualReadBody: true, 720 } 721 req := &codec.Body{} 722 rsp := &codec.Body{} 723 require.Nil(t, 724 c.Post(context.Background(), "/", req, rsp, 725 client.WithCurrentSerializationType(codec.SerializationTypeNoop), 726 client.WithSerializationType(codec.SerializationTypeNoop), 727 client.WithCurrentCompressType(codec.CompressTypeNoop), 728 client.WithRspHead(rspHead), 729 )) 730 require.Nil(t, rsp.Data) 731 body := rspHead.Response.Body // Do stream reads directly from rspHead.Response.Body. 732 defer body.Close() // Do remember to close the body. 733 bs, err := io.ReadAll(body) 734 require.Nil(t, err) 735 require.NotNil(t, bs) 736 } 737 738 type fileServer struct{} 739 740 func (*fileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 741 http.ServeFile(w, r, "./README.md") 742 return 743 } 744 ``` 745 746 747 ### 客户端服务端收发 HTTP chunked 748 749 1. 客户端发送 HTTP chunked: 750 1. 添加 `chunked` Transfer-Encoding header 751 2. 然后使用 io.Reader 进行发包 752 2. 客户端接收 HTTP chunked: Go 标准库 HTTP 自动支持了对 chunked 的处理, 上层用户对其是无感知的, 只需在 resp.Body 上面循环读直至 `io.EOF` (或者用 `io.ReadAll`) 753 3. 服务端读取 HTTP chunked: 和客户端读取类似 754 4. 服务端发送 HTTP chunked: 将 `http.ResponseWriter` 断言为 `http.Flusher`, 然后在每发送一部分数据后调用 `flusher.Flush()`, 这样就会自动触发 `chunked` encoding 从而发送出一个 chunk 755 756 示例如下: 757 758 ```go 759 func TestHTTPSendReceiveChunk(t *testing.T) { 760 // HTTP chunked example: 761 // 1. Client sends chunks: Add "chunked" transfer encoding header, and use io.Reader as body. 762 // 2. Client reads chunks: The Go/net/http automatically handles the chunked reading. 763 // Users can simply read resp.Body in a loop until io.EOF. 764 // 3. Server reads chunks: Similar to client reads chunks. 765 // 4. Server sends chunks: Assert http.ResponseWriter as http.Flusher, call flusher.Flush() after 766 // writing a part of data, it will automatically trigger "chunked" encoding to send a chunk. 767 768 // Start server. 769 const ( 770 network = "tcp" 771 address = "127.0.0.1:0" 772 ) 773 ln, err := net.Listen(network, address) 774 require.Nil(t, err) 775 defer ln.Close() 776 go http.Serve(ln, &chunkedServer{}) 777 778 // Start client. 779 c := thttp.NewClientProxy( 780 "trpc.app.server.Service_http", 781 client.WithTarget("ip://"+ln.Addr().String()), 782 ) 783 784 // Open and read file. 785 fileDir, err := os.Getwd() 786 require.Nil(t, err) 787 fileName := "README.md" 788 filePath := path.Join(fileDir, fileName) 789 file, err := os.Open(filePath) 790 require.Nil(t, err) 791 defer file.Close() 792 793 // 1. Client sends chunks. 794 795 // Add request headers. 796 header := http.Header{} 797 header.Add("Content-Type", "text/plain") 798 // Add chunked transfer encoding header. 799 header.Add("Transfer-Encoding", "chunked") 800 reqHead := &thttp.ClientReqHeader{ 801 Header: header, 802 ReqBody: file, // Stream send (for chunks). 803 } 804 805 // Enable manual body reading in order to 806 // disable the framework's automatic body reading capability, 807 // so that users can manually do their own client-side streaming reads. 808 rspHead := &thttp.ClientRspHeader{ 809 ManualReadBody: true, 810 } 811 req := &codec.Body{} 812 rsp := &codec.Body{} 813 require.Nil(t, 814 c.Post(context.Background(), "/", req, rsp, 815 client.WithCurrentSerializationType(codec.SerializationTypeNoop), 816 client.WithSerializationType(codec.SerializationTypeNoop), 817 client.WithCurrentCompressType(codec.CompressTypeNoop), 818 client.WithReqHead(reqHead), 819 client.WithRspHead(rspHead), 820 )) 821 require.Nil(t, rsp.Data) 822 823 // 2. Client reads chunks. 824 825 // Do stream reads directly from rspHead.Response.Body. 826 body := rspHead.Response.Body 827 defer body.Close() // Do remember to close the body. 828 buf := make([]byte, 4096) 829 var idx int 830 for { 831 n, err := body.Read(buf) 832 if err == io.EOF { 833 t.Logf("reached io.EOF\n") 834 break 835 } 836 t.Logf("read chunk %d of length %d: %q\n", idx, n, buf[:n]) 837 idx++ 838 } 839 } 840 841 type chunkedServer struct{} 842 843 func (*chunkedServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { 844 // 3. Server reads chunks. 845 846 // io.ReadAll will read until io.EOF. 847 // Go/net/http will automatically handle chunked body reads. 848 bs, err := io.ReadAll(r.Body) 849 if err != nil { 850 w.WriteHeader(http.StatusInternalServerError) 851 w.Write([]byte(fmt.Sprintf("io.ReadAll err: %+v", err))) 852 return 853 } 854 855 // 4. Server sends chunks. 856 857 // Send HTTP chunks using http.Flusher. 858 // Reference: https://stackoverflow.com/questions/26769626/send-a-chunked-http-response-from-a-go-server. 859 // The "Transfer-Encoding" header will be handled by the writer implicitly, so no need to set it. 860 flusher, ok := w.(http.Flusher) 861 if !ok { 862 w.WriteHeader(http.StatusInternalServerError) 863 w.Write([]byte("expected http.ResponseWriter to be an http.Flusher")) 864 return 865 } 866 chunks := 10 867 chunkSize := (len(bs) + chunks - 1) / chunks 868 for i := 0; i < chunks; i++ { 869 start := i * chunkSize 870 end := (i + 1) * chunkSize 871 if end > len(bs) { 872 end = len(bs) 873 } 874 w.Write(bs[start:end]) 875 flusher.Flush() // Trigger "chunked" encoding and send a chunk. 876 time.Sleep(500 * time.Millisecond) 877 } 878 return 879 } 880 ``` 881