blog icon indicating copy to clipboard operation
blog copied to clipboard

Http Trailer

Open ma6174 opened this issue 5 years ago • 5 comments

一般HTTP请求或响应包含HeaderBody,如果有些信息是在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有几个注意事项:

  1. Header里面的Transfer-Encoding必须是chunked,也就是说不能指定Content-Length
  2. Trailer 的字段名字必须在 Header里面提前声明,比如上面的Trailer: Expires
  3. TrailerBody发完之后再发,格式和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之后,能正常拿到TrailerMd5值。

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的值。

参考:

ma6174 avatar Mar 10 '19 10:03 ma6174

google 搜http trailer 一眼就看到马总

mingmingtsao avatar Jun 18 '19 08:06 mingmingtsao

来个坑 https://github.com/golang/go/issues/32935

Tevic avatar Jul 06 '19 08:07 Tevic

牛逼

itczl22 avatar Aug 31 '19 02:08 itczl22

解释的不错

sjatsh avatar Sep 09 '19 07:09 sjatsh

google 搜http trailer 一眼就看到马总 我也是google来此

chenyiping111 avatar Aug 24 '21 06:08 chenyiping111