blog
blog copied to clipboard
Http Trailer
一般HTTP请求或响应包含Header
和Body
,如果有些信息是在Body发完才知道,比如Body的校验、数字签名、后期处理结果等希望在同一个请求里面延后发送,就需要用到Trailer
。
传输格式
一个带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进行校验。
服务端程序:
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
的值。
参考:
google 搜http trailer 一眼就看到马总
来个坑 https://github.com/golang/go/issues/32935
牛逼
解释的不错
google 搜http trailer 一眼就看到马总 我也是google来此