本文主要讨论HTTP协议中的Transfer-Encoding。Transfer-Encoding,是一个 HTTP 头部字段,字面意思是「传输编码」。实际上,HTTP协议中还有另外一个头部与编码有关:Content-Encoding(内容编码)。Content-Encoding通常用于对实体内容进行压缩编码,目的是优化传输,例如用gzip压缩文本文件,能大幅减小体积。内容编码通常是选择性的,例如jpg/png
这类文件一般不开启,因为图片格式已经是高度压缩过的,再压一遍没什么效果不说还浪费CPU。
而Transfer-Encoding则是用来改变报文格式(这个可能你现在还不理解,先看后面),它不但不会减少实体内容传输大小,甚至还会使传输变大,那它的作用是什么呢?本文接下来主要就是讲这个。我们先记住一点,Content-Encoding和Transfer-Encoding二者是相辅相成的,对于一个HTTP报文,很可能同时进行了内容编码和传输编码。
长连接
暂时把Transfer-Encoding放一边,我们来看HTTP协议中另外一个重要概念:Persistent Connection(持久连接,通俗说法长连接)。我们知道HTTP运行在TCP连接之上,自然也有着跟TCP一样的三次握手、慢启动等特性,为了尽可能的提高HTTP性能,使用持久连接就显得尤为重要了。为此,HTTP协议引入了相应的机制。
HTTP/1.0的持久连接机制是后来才引入的,通过Connection: keep-alive这个头部来实现,服务端和客户端都可以使用它告诉对方在发送完数据之后不需要断开TCP连接,以备后用。HTTP/1.1 则规定所有连接都必须是持久的,除非显式地在头部加上Connection: close。所以实际上,HTTP/1.1 中 Connection这个头部字段已经没有keep-alive这个取值了,但由于历史原因,很多Web Server和浏览器,还是保留着给HTTP/1.1长连接发送Connection: keep-alive的习惯。
但是这也带来了另外一个问题,长连接怎么判断一次请求的数据传输完成?于非持久连接,浏览器可以通过连接是否关闭来界定请求或响应实体的边界;而对于持久连接,这种方法显然不奏效。因此引入了下面一个字段。
Content-Length
要解决上面这个问题,最容易想到的办法就是计算实体长度,并通过头部告诉对方。这就要用到 Content-Length。浏览器可以通过Content-Length
的长度信息,判断出响应实体已结束。那如果 Content-Length和实体实际长度不一致会怎样?通常如果Content-Length
比实际长度短,会造成内容被截断;如果比实体内容长,会造成pending。
由于Content-Length字段必须真实反映实体长度,但实际应用中,有些时候实体长度并没那么好获得,例如实体来自于网络文件,或者由动态语言生成。这时候要想准确获取长度,只能开一个足够大的 buffer,等内容全部生成好再计算。但这样做一方面需要更大的内存开销,另一方面也会让客户端等更久。
我们在做WEB性能优化时,有一个重要的指标叫TTFB(Time To First Byte),它代表的是从客户端发出请求到收到响应的第一个字节所花费的时间。大部分浏览器自带的Network面板都可以看到这个指标,越短的TTFB意味着用户可以越早看到页面内容,体验越好。可想而知,服务端为了计算响应实体长度而缓存所有内容,跟更短的TTFB理念背道而驰。但在HTTP报文中,实体一定要在头部之后,顺序不能颠倒,为此我们需要一个新的机制:不依赖头部的长度信息,也能知道实体的边界。
Transfer-Encoding: chunked
本文主角终于再次出现了,Transfer-Encoding正是用来解决上面这个问题的。历史上 Transfer-Encoding可以有多种取值,为此还引入了一个名为TE的头部用来协商采用何种传输编码。但是最新的HTTP规范里,只定义了一种传输编码:分块编码(chunked)。
分块编码相当简单,在头部加入Transfer-Encoding: chunked之后,就代表这个报文采用了分块编码。这时,报文中的实体需要改为用一系列分块来传输。每个分块包含十六进制的长度值和数据,长度值独占一行,长度不包括它结尾的 CRLF(\r\n),也不包括分块数据结尾的CRLF。最后一个分块长度值必须为0,对应的分块数据没有内容,表示实体结束。
前面说过 Content-Encoding 和 Transfer-Encoding 二者经常会结合来用,其实就是针对进行了内容编码(压缩)的内容再进行传输编码(分块)。