关于http:HTTP-协议中的-TransferEncoding

9次阅读

共计 3481 个字符,预计需要花费 9 分钟才能阅读完成。

Transfer-Encoding,是一个 HTTP 头部字段,字面意思是「传输编码」。实际上,HTTP 协定中还有另外一个头部与编码无关:Content-Encoding(内容编码)。Content-Encoding 通常用于对实体内容进行压缩编码,目标是优化传输,例如用 gzip 压缩文本文件,能大幅减小体积。内容编码通常是选择性的,例如 jpg / png 这类文件个别不开启,因为图片格式曾经是高度压缩过的,再压一遍没什么成果不说还节约 CPU

Transfer-Encoding 则是用来扭转报文格式,它岂但不会缩小实体内容传输大小,甚至还会使传输变大,那它的作用是什么呢?本文接下来次要就是讲这个。咱们先记住一点,Content-EncodingTransfer-Encoding 二者是相辅相成的,对于一个 HTTP 报文,很可能同时进行了内容编码和传输编码。

Persistent Connection

临时把 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 的习惯。

浏览器重用曾经关上的闲暇长久连贯,能够避开迟缓的三次握手,还能够防止遇上 TCP 慢启动的拥塞适应阶段,听起来非常美好。为了深入研究长久连贯的个性,我决定用 Node 写一个最简略的 Web Server 用于测试,Node 提供了 http 模块用于疾速创立 HTTP Web Server,但我须要更多的管制,所以用 net 模块创立了一个 TCP Server

require('net').createServer(function(sock) {sock.on('data', function(data) {sock.write('HTTP/1.1 200 OKrn');
        sock.write('rn');
        sock.write('hello world!');
        sock.destroy();});
}).listen(9090, '127.0.0.1'); 

启动服务后,在浏览器里拜访 127.0.0.1:9090,正确输入了指定内容,一切正常。去掉 sock.destroy() 这一行,让它变成长久连贯,重启服务后再拜访一下。这次的后果就有点奇怪了:迟迟看不到输入,通过 Network 查看申请状态,始终是 pending

这是因为,对于非长久连贯,浏览器能够通过连贯是否敞开来界定申请或响应实体的边界;而对于长久连贯,这种办法显然不见效。上例中,只管我曾经发送完所有数据,但浏览器并不知道这一点,它无奈得悉这个关上的连贯上是否还会有新数据进来,只能傻傻地等了。

Content-Length

要解决下面这个问题,最容易想到的方法就是计算实体长度,并通过头部通知对方。这就要用到 Content-Length 了,革新一下下面的例子:

require('net').createServer(function(sock) {sock.on('data', function(data) {sock.write('HTTP/1.1 200 OKrn');
        sock.write('Content-Length: 12rn');
        sock.write('rn');
        sock.write('hello world!');
    });
}).listen(9090, '127.0.0.1');

能够看到,这次发送完数据并没有敞开 TCP 连贯,但浏览器能失常输入内容并完结申请,因为浏览器能够通过 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(rn),也不包含分块数据结尾的 CRLF。最初一个分块长度值必须为 0,对应的分块数据没有内容,示意实体完结。依照这个格局革新下之前的代码:

require('net').createServer(function(sock) {sock.on('data', function(data) {sock.write('HTTP/1.1 200 OKrn');
        sock.write('Transfer-Encoding: chunkedrn');
        sock.write('rn');

        sock.write('brn');
        sock.write('01234567890rn');

        sock.write('5rn');
        sock.write('12345rn');

        sock.write('0rn');
        sock.write('rn');
    });
}).listen(9090, '127.0.0.1');

下面这个例子中,我在响应头中表明接下来的实体会采纳分块编码,而后输入了 11 字节的分块,接着又输入了 5 字节的分块,最初用一个 0 长度的分块表明数据曾经传完了。用浏览器拜访这个服务,能够失去正确后果。能够看到,通过这种简略的分块策略,很好的解决了后面提出的问题。

后面说过 Content-EncodingTransfer-Encoding 二者常常会联合来用,其实就是针对进行了内容编码(压缩)的内容再进行传输编码(分块)。上面是我用 telnet 申请测试页面失去的响应,能够看到对 gzip 内容进行的分块:

> telnet 106.187.88.156 80

GET /test.php HTTP/1.1
Host: qgy18.qgy18.com
Accept-Encoding: gzip

HTTP/1.1 200 OK
Server: nginx
Date: Sun, 03 May 2015 17:25:23 GMT
Content-Type: text/html
Transfer-Encoding: chunked
Connection: keep-alive
Content-Encoding: gzip

1f
�H���W(�/�I�J

0

用 HTTP 抓包神器 Fiddler 也能够看到相似后果,有趣味的同学能够本人试一下。

正文完
 0