HTTP-Trailer
Last updated: 2020/10/20 Published at: 2020/10/20
Concept
Trailer 是一个响应首部,允许发送方在分块发送的消息后面添加额外的元信息,这些元信息可能是随着消息主体的发送动态生成的,比如消息的完整性校验,消息的数字签名,或者消息经过处理之后的最终状态等。
Grammar
Trailer: header-names
header-names 是出现在分块信息挂载部分的消息首部。以下首部字段不允许出现:
- 用于信息分帧的首部 (例如
Transfer-Encoding
和Content-Length
), - 用于路由用途的首部 (例如
Host
), - 请求修饰首部 (例如控制类和条件类的,如
Cache-Control
,Max-Forwards
,或者TE
), - 身份验证首部 (例如
Authorization
或者Set-Cookie
), Content-Encoding
,Content-Type
,Content-Range
,以及Trailer
自身。
Example
1HTTP/1.1 200 OK
2Content-Type: text/plain
3Transfer-Encoding: chunked
4Trailer: Expires
5
67\r\n
7Mozilla\r\n
89\r\n
9Developer\r\n
107\r\n
11Network\r\n
120\r\n
13Expires: Wed, 21 Oct 2015 07:28:00 GMT\r\n
14\r\n
解释:
Header
里面的Transfer-Encoding
必须是chunked
,也就是说不能指定Content-Length
。Trailer
的字段名字必须在Header
里面提前声明,比如上面的Trailer: Expires
。Trailer
在Body
发完之后再发,格式和Header
类似。
Implement in Go
用 Go 实现一个 HTTP 客户端,对所发的 Body
计算 MD5
并通过 Trailer
传给服务端。服务端收到请求并对 Body 进行校验。
Server Code
1package main
2
3import (
4 "crypto/md5"
5 "fmt"
6 "io/ioutil"
7 "log"
8 "net/http"
9)
10
11func checkMd5(w http.ResponseWriter, req *http.Request) {
12 fmt.Printf("Header:%+v\n", req.Header)
13 fmt.Println("Trailer before read body:")
14 fmt.Println(req.Trailer)
15
16 data, err := ioutil.ReadAll(req.Body)
17 bodyMd5 := fmt.Sprintf("%x", md5.Sum(data))
18 defer req.Body.Close()
19
20 fmt.Println("body:", string(data))
21 fmt.Println("md5:", bodyMd5)
22 fmt.Println("error:", err)
23 fmt.Println("Trailer after read body:")
24 fmt.Println(req.Trailer)
25
26 if req.Trailer.Get("md5") != bodyMd5 {
27 panic("body md5 not equal")
28 }
29}
30
31func main() {
32 http.HandleFunc("/", checkMd5)
33 log.Fatal(http.ListenAndServe(":12345",nil))
34}
Client Code
1package main
2
3import (
4 "crypto/md5"
5 "fmt"
6 "hash"
7 "io"
8 "net/http"
9 "os"
10 "strconv"
11 "strings"
12)
13
14type headerReader struct {
15 reader io.Reader
16 md5 hash.Hash
17 header http.Header
18}
19
20func (r *headerReader) Read(p []byte) (n int, err error) {
21 n, err = r.reader.Read(p)
22 if n > 0 {
23 r.md5.Write(p[:n])
24 }
25 if err == io.EOF {
26 r.header.Set("md5", fmt.Sprintf("%x", r.md5.Sum(nil)))
27 }
28 return
29}
30
31func main() {
32 h := &headerReader{
33 reader: strings.NewReader("body"),
34 md5: md5.New(),
35 header: http.Header{"md5": nil, "size": []string{strconv.Itoa(len("body"))}},
36 }
37 req, err := http.NewRequest("POST", "http://localhost:12345", h)
38 if err != nil {
39 panic(err)
40 }
41 req.ContentLength = -1
42 req.Trailer = h.header
43 resp, err := http.DefaultClient.Do(req)
44 if err != nil {
45 panic(err)
46 }
47 fmt.Println(resp.Status)
48 _, err = io.Copy(os.Stdout, resp.Body)
49 if err != nil {
50 panic(err)
51 }
52}
Result
Server result
$ 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
md5: 841a2d689ad86bd1611447453c22c6fc
error: <nil>
Trailer after read body:
map[Md5:[841a2d689ad86bd1611447453c22c6fc] Size:[4]]
Client result
$ go run client.go
200 OK
nc result
$ nc -l 12345
POST / HTTP/1.1
Host: localhost:12345
User-Agent: Go-http-client/1.1
Transfer-Encoding: chunked
Trailer: Md5,Size
Accept-Encoding: gzip
4
body
0
Md5: 841a2d689ad86bd1611447453c22c6fc
size: 4
Conclusion
可以看到服务端在读完 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
的值。