学习 HTTP 时的一些笔记,记录下来。

Transfer-Encoding 头表示 HTTP 消息体的传输方式。

其作用与 Content-Encoding 不同,Content-Encoding 的值是作用于整个 HTTP 实体,而 Transfer-Encoding 只作用于消息体,并不会作用于消息头部。

MDN 上有对 Transfer-Encoding 做一些解释,这里主要记录一下 Transfer-Encoding: chunked 这种情况。

我们用的 HTTP 是一般情况下是建立在 TCP 上的,而 TCP 返回的是一个无边界的数据流,如果要正确地解析 HTTP,则需要知道 HTTP 的边界在哪里。

在 HTTP 1.1 之前,是 EOF(end of file),也就是关闭 TCP 链接。这种方式的好处是实现简单,只需要在读取 socket 时,判断如果读取到了 EOF,即可停止读取,关闭连接,再把读取到的数据返回给 HTTP 应用程序。坏处也显然易见,在使用浏览器浏览同一个网站时,请求同一台服务器的静态资源文件通常会有多个,如果每次请求一个静态资源成功即关闭 TCP 链接,第二次请求同一台服务器下的另一个静态资源,则需要再一次发起 TCP 连接请求以传输数据。由于 TCP 链接需要时间,在请求数多的情况下,会让用户感觉页面响应慢,影响体验。

为了改善这个问题,HTTP 1.1 版本引入了 Connection: keep-alive 头,该头表示保持连接状态。在第一次请求静态资源结束后,并不会关闭 TCP 链接,而是继续保持连接,继续进行后续的 HTTP 请求的传输。

现在所有 HTTP 的请求都在同一个通道中进行,那要如何确定上一个 HTTP 请求体已结束,从而接受下一个 HTTP 请求体呢?

有 Content-Length。

Content-Length 头表示的是 HTTP 消息体的字节长度,不包括消息头部的长度。有了这个头,接收方在解析消息体时就可以根据 Content-Length 的长度知道应该在何处结束解析。

Content-Length 看似很完美,但是也会有性能问题。Content-Length 表示的消息体的长度,所以发送方必须获取到整个消息体的内容才能计算出 Content-Length 的长度,如果发送的消息体很长,则发送方缓存消息体需要的空间就会很大。比如查询数据库后把数据组装成一个很大的 JSON 字符串时。

合理的处理方式应该是有多少数据就发送多少数据,而不需要知晓整个消息体有多少数据量,只要有数据就尽力发送到接收方。

Transfer-Encoding: chunked 表示的就是这样的一种方式。HTTP 的消息体会以块(chunk)的形式分多次发送,具体的数据格式在 MDN 有提到,我做一些简单修改:

1
2
3
4
5
6
7
8
9
10
11
12
HTTP/1.1 200 OK\r\n
Content-Type: text/plain\r\n
Transfer-Encoding: chunked\r\n
\r\n
1B\r\n
Hello world! I love Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
\r\n

发送方在发送的 HTTP 实体头部中定义了 Transfer-Encoding: chunked,表示这一次发送将会以块的形式分多次发送,在发送完了所有头部信息之后,发送方继续发送数据块。第一个数据块是:

1
2
1B\r\n
Hello world! I love Mozilla\r\n

1B 是十六进制数字,表示本次发送的数据块的字节长度。接收方接收到块之后就开始解析,并把真正的数据拼接到预先准备好的缓冲区中,此处的有效数据是 Hello world! I love Mozilla。接着接收方继续解析到达的数据块,直到接收到最后一个数据块:

1
2
0\r\n
\r\n

这是最后的数据块,表示后面已经没有数据块了,解析到这个块则表示 HTTP 消息体已全部发送,接下来发送方还可以继续发送一些头部,在 RFC 中被称为 Trailer,其实是一些头部信息,格式也跟普通的头部信息一样。所以发送方发送的整个实体可能是这样的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
HTTP/1.1 200 OK\r\n
Content-Type: text/plain\r\n
Transfer-Encoding: chunked\r\n
\r\n
1B\r\n
Hello world! I love Mozilla\r\n
9\r\n
Developer\r\n
7\r\n
Network\r\n
0\r\n
\r\n
Server: AmazonS3\r\n
Set-Cookie: xxx=1; Path=/; HttpOnly\r\n
\r\n

RFC 的中也给出了一个伪代码的实现(RFC 7230 section-4.1.3):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
length := 0
read chunk-size, chunk-ext (if any), and CRLF
while (chunk-size > 0) {
read chunk-data and CRLF
append chunk-data to decoded-body
length := length + chunk-size
read chunk-size, chunk-ext (if any), and CRLF
}
read trailer field
while (trailer field is not empty) {
if (trailer field is allowed to be sent in a trailer) {
append trailer field to existing header fields
}
read trailer-field
}
Content-Length := length
Remove "chunked" from Transfer-Encoding
Remove Trailer from existing header fields

首先定义一个变量 length = 0;然后开始循环读取数据块,同时根据解析后的数据块的长度,递增 length 变量的值,直到读取到最后一个数据块,再继续循环读取 trailer 的信息,直到读取到空的头信息(\r\n),最后把 length 赋值给 Content-Length 头,并从 Transfer-Encoding 中删除 chunked,删除 trailer 中与 header 重复的 fields。

还有更复杂的情况是,每一个数据块都可以使用不同的编码方式进行编码和解码。

参考

RFC 7230

MDN Transfer-Encoding