追踪 HTTP Trailer

发布于 2022-09-27 13:12:13 字数 4523 浏览 132 评论 0

trailer
美: [ˈtreɪlər]
英: [ˈtreɪlə(r)]
n. 挂车;拖车;活动工作室;(电影或电视节目的)预告片

缘起

要在 API 网关(基于 openresty 的 lua 实现)上增加超时设置,谷歌搜索了一下 openresty http,找到了 ledgetech/lua-resty-http,看了一下它的 README 提到的特征列表:

  • HTTP 1.0 and 1.1
  • SSL
  • Streaming interface to the response body, for predictable memory usage
  • Alternative simple interface for singleshot requests without manual connection step
  • Chunked and non-chunked transfer encodings
  • Keepalive
  • Pipelining
  • Trailers

前面几项,大概都懂,唯独对于 Trailers,感觉是闻所未闻。因此,抽空,考究了一下。

作用

一般 HTTP 请求或响应包含 Header 和 Body,如果有些信息是在 Body 发完才知道,比如 Body 的校验、数字签名、后期处理结果等希望在同一个请求里面延后发送,就需要用到 Trailer。Trailer 是 HTTP/1.1 定义的。

例子

一个带 Trailer 的响应例子:

HTTP/1.1 200 OK 
Content-Type: text/plain 
Transfer-Encoding: chunked
Trailer: Expires

7\r\n 
Mozilla\r\n 
9\r\n 
Developer\r\n 
7\r\n 
Network\r\n 
0\r\n 
Expires: Wed, 21 Oct 2015 07:28:00 GMT\r\n
\r\n

使用 Trailer 有几个注意事项:

  1. Header 里面的 Transfer-Encoding 必须是 chunked,也就是说不能指定 Content-Length。
  2. Trailer 的字段名字必须在 Header 里面提前声明,比如上面的 Trailer: Expires。
  3. Trailer 在 Body 发完之后再发,格式和 Header 类似。

实战

用 Go 实现一个 HTTP 客户端,对所发的 Body 计算 MD5 并通过 Trailer 传给服务端。
服务端收到请求并对 Body 进行校验。

Go 实现的带有 Trailer 的客户端请求

服务端程序:

package main

import (
  "crypto/md5"
  "fmt"
  "io/ioutil"
  "log"
  "net/http"
)

func index(w http.ResponseWriter, r *http.Request) {
  fmt.Printf("header: %+v\n", r.Header)
  fmt.Printf("trailer before read body: %+v\n", r.Trailer)
  data, err := ioutil.ReadAll(r.Body)
  bodyMd5 := fmt.Sprintf("%x", md5.Sum(data))
  fmt.Printf("body: %v,body md5: %v, err: %v\n", string(data), bodyMd5, err)
  fmt.Printf("trailer after read body: %+v\n", r.Trailer)
  if r.Trailer.Get("md5") != bodyMd5 {
    panic("body md5 not equal")
  }
}

func main() {
  http.HandleFunc("/", index)
  log.Fatal(http.ListenAndServe(":1235", nil))
}

客户端程序:

package main

import (
  "crypto/md5"
  "fmt"
  "hash"
  "io"
  "net/http"
  "os"
  "strconv"
  "strings"
)

type headerReader struct {
  reader io.Reader
  md5    hash.Hash
  header http.Header
}

func (r *headerReader) Read(p []byte) (n int, err error) {
  n, err = r.reader.Read(p)
  if n > 0 {
    r.md5.Write(p[:n])
  }
  if err == io.EOF {
    r.header.Set("md5", fmt.Sprintf("%x", r.md5.Sum(nil)))
  }
  return
}

func main() {
  h := &headerReader{
    reader: strings.NewReader("body"),
    md5:    md5.New(),
    header: http.Header{"md5": nil, "size": []string{strconv.Itoa(len("body"))}},
  }
  req, err := http.NewRequest("POST", "http://localhost:1235", h)
  if err != nil {
    panic(err)
  }
  req.ContentLength = -1
  req.Trailer = h.header
  resp, err := http.DefaultClient.Do(req)
  if err != nil {
    panic(err)
  }
  fmt.Println(resp.Status)
  _, err = io.Copy(os.Stdout, resp.Body)
  if err != nil {
    panic(err)
  }
}

运行结果:

$ go run server.go
header: map[Accept-Encoding:[gzip] User-Agent:[Go-http-client/1.1]]
trailer before read body: map[Md5:[] Size:[]]
body: body,body md5: 841a2d689ad86bd1611447453c22c6fc, err: <nil>
trailer after read body: map[Md5:[841a2d689ad86bd1611447453c22c6fc] Size:[4]]

$ go run client.go
200 OK

通过 nc 来看服务端收到的请求

$ nc -l 1235
POST / HTTP/1.1
Host: localhost:1235
User-Agent: Go-http-client/1.1
Transfer-Encoding: chunked
Trailer: Md5,Size
Accept-Encoding: gzip

4
body
0
Md5: 841a2d689ad86bd1611447453c22c6fc
size: 4

可以看到服务端在读完 body 之前只能知道有 Md5 这个 Trailer,值为空;读完 body 之后,能正常拿到 Trailer 的 Md5 值。

Go 语言使用 Trailer 也有几个注意事项:

  1. req.ContentLength 必须设置为 0 或者 -1,这样 body 才会以 chunked 的形式传输。
  2. req.Trailer 需要在发请求之前声明所有的 key 字段,在 body 发完之后设置相应的 value,如果客户端提前知道 Trailer 的值的话也可以提前设置,比如上面例子里面的 size 字段。
  3. 发完 body 之后 Trailer 不允许再更改,否则可能会因为 map 并发读写,导致程序 panic,同样的道理服务端在读 body 的时候也不应该对 Trailer 有引用。
  4. 服务端必须读完 body 之后才能知道 Trailer 的值。

参考

  1. https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer
  2. https://cloud.tencent.com/developer/section/1190006
  3. golang/go:src/net/http/request.go@f1d662f#L257-L275

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据

关于作者

文章
评论
28 人气
更多

推荐作者

櫻之舞

文章 0 评论 0

弥枳

文章 0 评论 0

m2429

文章 0 评论 0

野却迷人

文章 0 评论 0

我怀念的。

文章 0 评论 0

    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文