Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Http Trailer #22

Open
ma6174 opened this issue Mar 10, 2019 · 5 comments
Open

Http Trailer #22

ma6174 opened this issue Mar 10, 2019 · 5 comments

Comments

@ma6174
Copy link
Owner

ma6174 commented Mar 10, 2019

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

参考:

@mingmingtsao
Copy link

google 搜http trailer 一眼就看到马总

@Tevic
Copy link

Tevic commented Jul 6, 2019

来个坑
golang/go#32935

@itczl22
Copy link

itczl22 commented Aug 31, 2019

牛逼

@sjatsh
Copy link

sjatsh commented Sep 9, 2019

解释的不错

@chenyiping111
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

6 participants