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