# HTTP2 下的 Transfer-Encoding: chunked 在 HTTP 中传输数据有一个 chunked 的方式, 又称“分块传输”。在响应报文里用头字段Transfer-Encoding: chunked 来表示。意思是报文里的 body 部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。而 HTTP2.0 协议作为 HTTP协议的升级,自然是对chunked模式做支持?不然! **HTTP2 是没有 chunked 的!** 分块传输也可以用于“流式数据”,例如由数据库动态生成的表单页面,这种情况下 body 数据的长度是未知的,无法在头字段“Content-Length”里给出确切的长度,所以也只能用 chunked 方式分块发送。 # chunked 的编码规则 - 每个分块包含两个部分,长度头和数据块; - 长度头是以 CRLF(回车换行,即\r\n)结尾的一行明文,用 16 进制数字表示长度; - 数据块紧跟在长度头后,最后也用 CRLF 结尾,但数据不包含 CRLF; - 最后用一个长度为 0 的块表示结束,即“0\r\n\r\n” # HTTP2 下的分块传输 先说结论,HTTP2 是不支持 HTTP1 语义下的 chunked 模式的。因为H2的 Data 帧是纯天然的Chunked模式。 这也是最容易出bug的地方,一些实现不完全的HTTP2开源库经常在这里出问题。因为我们线上都是客户端请求基本都是 chunked 模式,升级到 HTTP2 之后,经常访问一些三方链接访问卡死,最终 debug 后的原因发现出在服务端对 HTTP2 chunked  的支持上。 最常见的反向代理实现 Nginx 就是最容易有这种 bug的,很多企业维护的Nginx经常不更新,而低版本的NginxHTTP2上的这个bug就被我们遇到过。 具体现象是,客户端使用chunked模式上传,但是服务端开启了 HTTP2,自然客户端也就升级到 HTTP2 。但是请求总是卡主,服务端无响应。最终定位到如果 DATA帧不携带内容,只携带一个 End Stream 标志,服务端无法识别流式传输结束。我拿 `www.bing.com` 做了对比,其他网站都是正常的。 其实 RFC 7540 早有规定,HTTP2 的传输是不支持 Transfer-Encoding: chunked 的。 一般的网络库(如 CronetOKHTTP )底层都给我们做了兼容,如果上层使用 chunked 模式传输,而实际使用的是 HTTP2 ,网络库会帮我们自动隐藏掉 Transfer-Encoding: chunked 这个 Header 。 # 使用 HTTP2 发送 chunked 这里我们以 Golang 的 HTTP2 官方的客户端库做一个测试. ``` golang func main() { rd, wr := io.Pipe() u, _ := url.Parse("https://httpbin.org/post") req := &http.Request{ Method: "POST", ProtoMajor: 1, ProtoMinor: 1, URL: u, TransferEncoding: []string{"chunked"}, Body: rd, Header: make(map[string][]string), } req.Header.Set("Content-Type", "application/x-protobuf") var client *http.Client var transport *http2.Transport sslkeylogfile, err := os.OpenFile("/tmp/sslkey.log", os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0666) if err != nil { panic(err) } defer sslkeylogfile.Close() transport = &http2.Transport{} var config *tls.Config = &tls.Config{ InsecureSkipVerify: true, KeyLogWriter: sslkeylogfile, } transport.TLSClientConfig = config client = &http.Client{Transport: transport} go func() { buf := make([]byte, 8000) str := "" for i := 0; i < 100000; i++ { str += "0" } f := strings.NewReader(str) for { n, _ := f.Read(buf) if 0 == n { break } wr.Write(buf) } wr.Close() }() resp, err := client.Do(req) if nil != err { fmt.Println("error =>", err.Error()) return } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if nil != err { fmt.Println("error =>", err.Error()) } else { fmt.Println(string(body)) } } ``` 通过导入 SSLKEYLOGWireshark 抓包可以看到,每个 Data Frame 大小为 8000 。