追踪 HTTP Trailer
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 有几个注意事项:
- Header 里面的 Transfer-Encoding 必须是 chunked,也就是说不能指定 Content-Length。
- Trailer 的字段名字必须在 Header 里面提前声明,比如上面的 Trailer: Expires。
- 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 也有几个注意事项:
- req.ContentLength 必须设置为 0 或者 -1,这样 body 才会以 chunked 的形式传输。
- req.Trailer 需要在发请求之前声明所有的 key 字段,在 body 发完之后设置相应的 value,如果客户端提前知道 Trailer 的值的话也可以提前设置,比如上面例子里面的 size 字段。
- 发完 body 之后 Trailer 不允许再更改,否则可能会因为 map 并发读写,导致程序 panic,同样的道理服务端在读 body 的时候也不应该对 Trailer 有引用。
- 服务端必须读完 body 之后才能知道 Trailer 的值。
参考
- https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Trailer
- https://cloud.tencent.com/developer/section/1190006
- golang/go:src/net/http/request.go@f1d662f#L257-L275
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

上一篇: 应用程序日志规范
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论