4.3.2 传输压缩
我们接下来讨论链路优化中缓存、连接之外的另一个主要话题:压缩。同时也是为了解决上一节遗留的问题:如何不以断开TCP连接为标志来判断资源已传输完毕。
HTTP很早就支持了GZip压缩,因为HTTP传输的内容主要是文本数据,譬如HTML、CSS、Script等,而对于文本数据启用压缩的收益是非常高的,传输数据量一般会降至原有的20%左右。对于那些不适合压缩的资源,Web服务器能根据MIME类型自动判断是否对响应进行压缩,这样,已经采用过压缩算法存储的资源,如JPEG、PNG图片,便不会被二次压缩,空耗性能。
不过,大概就没有多少人想过压缩与之前提到的用于节约TCP的持久连接机制是存在冲突的。在网络时代的早期,服务器处理能力还很薄弱,为了启用压缩,会把静态资源先预先压缩为.gz文件的形式存放起来,当客户端可以接收压缩版本的资源时(请求的Header中包含Accept-Encoding:gzip)就返回压缩后的版本(响应的Header中包含Content-Encoding:gzip),否则返回未压缩的原版,这种方式被称为“静态预压缩”(Static Precompression)。而现代的Web服务器处理能力有了大幅提升,已经没有人再采用麻烦的预压缩方式了,都是由服务器对符合条件的请求在输出时进行“即时压缩”(On-The-Fly Compression),整个压缩过程全部在内存的数据流中完成,不必等资源压缩完成再返回响应,这样可以显著提高“首字节时间”(Time To First Byte,TTFB),改善Web性能体验。而这个过程中唯一不好的地方就是服务器再也没有办法给出Content-Length这个响应Header了,因为输出Header时服务器还不知道压缩后资源的确切大小。
到这里,大家了解即时压缩与持久连接的冲突在哪了吗?持久连接机制不再依靠TCP连接是否关闭来判断资源请求是否结束,它会重用同一个连接以便向同一个域名请求多个资源,这样,客户端就必须要有除了关闭连接之外的其他机制来判断一个资源什么时候算传递完毕,这个机制最初(在HTTP/1.0时)就只有Content-Length,即依靠请求Header中明确给出资源的长度判断,传输到达该长度即宣告一个资源的传输已结束。由于启用即时压缩后就无法给出Content-Length了,如果是HTTP/1.0的话,持久连接和即时压缩只能二选一,事实上在HTTP/1.0中对两者都支持,却默认都是不启用的。依靠Content-Length来判断传输结束的缺陷,不仅仅在于即时压缩这一种场景,譬如对于动态内容(Ajax、PHP、JSP等输出),服务器也同样无法事先得知Content-Length。
HTTP/1.1版本中修复了这个缺陷,增加了另一种“分块传输编码”(Chunked Transfer Encoding)的资源结束判断机制,彻底解决了Content-Length与持久连接的冲突问题。分块编码的原理相当简单:在响应Header中加入“Transfer-Encoding:chunked”之后,就代表这个响应报文将采用分块编码。此时,报文中的Body需要改为用一系列“分块”来传输。每个分块包含十六进制的长度值和对应长度的数据内容,长度值独占一行,数据从下一行开始,最后以一个长度值为0的分块来表示资源结束。举个具体例子[1]:
HTTP/1.1 200 OK Date: Sat, 11 Apr 2020 04:44:00 GMT Transfer-Encoding: chunked Connection: keep-alive 25 This is the data in the first chunk 1C and this is the second one 3 con 8 sequence 0
根据分块长度可知,前两个分块包含显式的回车换行符(CRLF,即\r\n字符)。
"This is the data in the first chunk\r\n" (37 字符 => 十六进制: 0x25) "and this is the second one\r\n" (28 字符 => 十六进制: 0x1C) "con" (3 字符 => 十六进制: 0x03) "sequence" (8 字符 => 十六进制: 0x08)
所以解码后的内容为:
This is the data in the first chunk and this is the second one consequence
一般来说,Web服务器给出的数据分块大小应该(但并不强制)是一致的,而不是如例子中那样随意。HTTP/1.1通过分块传输解决了即时压缩与持久连接并存的问题,到了HTTP/2,由于多路复用和单域名单连接的设计,已经无须再刻意去提持久连接机制了,但数据压缩仍然有节约传输带宽的重要价值。
[1] 例子来自于维基百科,为便于观察,只分块,未压缩。