Warning
本文最后更新于 February 28, 2022,文中内容可能已过时,请谨慎使用。
在 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经常不更新,而低版本的Nginx在HTTP2上的这个bug就被我们遇到过。
具体现象是,客户端使用chunked模式上传,但是服务端开启了 HTTP2,自然客户端也就升级到 HTTP2 。但是请求总是卡主,服务端无响应。最终定位到如果 DATA帧不携带内容,只携带一个 End Stream 标志,服务端无法识别流式传输结束。我拿 www.bing.com
做了对比,其他网站都是正常的。
其实 RFC 7540 早有规定,HTTP2 的传输是不支持 Transfer-Encoding: chunked 的。
一般的网络库(如 Cronet、OKHTTP )底层都给我们做了兼容,如果上层使用 chunked 模式传输,而实际使用的是 HTTP2 ,网络库会帮我们自动隐藏掉 Transfer-Encoding: chunked 这个 Header 。
使用 HTTP2 发送 chunked
这里我们以 Golang 的 HTTP2 官方的客户端库做一个测试.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
| 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))
}
}
|
通过导入 SSLKEYLOG 从 Wireshark 抓包可以看到,每个 Data Frame 大小为 8000 。