trailer
美: [ˈtreɪlər]
英: [ˈtreɪlə(r)]
n. 挂车;拖车;活动工作室;(电影或电视节目的)预告片
要在 API 网关(基于 openresty 的 lua 实现)上增加超时设置,谷歌搜索了一下 openresty http,找到了 ledgetech/lua-resty-http,看了一下它的 README 提到的特征列表:
前面几项,大概都懂,唯独对于 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 有几个注意事项:
用 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 也有几个注意事项:

