Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1026133
  • 博文数量: 157
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 1388
  • 用 户 组: 普通用户
  • 注册时间: 2015-04-09 15:37
文章分类

全部博文(157)

文章存档

2023年(9)

2022年(2)

2021年(18)

2020年(7)

2017年(13)

2016年(53)

2015年(55)

我的朋友

分类: 网络与安全

2015-10-15 16:15:20

转载于:htrequire('net').createServer(function(sock) { sock.on('data', function(data) { sock.write('HTTP/1.1 200 OK\r\n'); sock.write('\r\n'); 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 OK\r\n');   sock.write('Content-Length: 12\r\n');   sock.write('\r\n');   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(\r\n),也不包括分块数据结尾的 CRLF。最后一个分块长度值必须为 0,对应的分块数据没有内容,表示实体结束。按照这个格式改造下之前的代码:

require('net').createServer(function(sock) {
    sock.on('data', function(data) {  sock.write('HTTP/1.1 200 OK\r\n');  sock.write('Transfer-Encoding: chunked\r\n');  sock.write('\r\n');  sock.write('b\r\n');  sock.write('01234567890\r\n');  sock.write('5\r\n');  sock.write('12345\r\n');  sock.write('0\r\n');  sock.write('\r\n');
    });
}).listen(9090, '127.0.0.1');  

上面这个例子中,我在响应头中表明接下来的实体会采用分块编码,然后输出了 11 字节的内容,接着又输出了 5 字节内容,最后用一个 0 长度的分块表明数据已经传完了。用浏览器访问这个服务,可以得到正确结果。可以看到,通过这种简单的分块策略,很好的解决了前面提出的问题。

前面说过 Content-Encoding 和 Transfer-Encoding 二者经常会结合来用,其实就是针对 Transfer-Encoding 的分块再进行 Content-Encoding。下面是我用 telnet 请求测试页面得到的响应,就对分块内容进行了 gzip 编码:

telnet 106.187.88.156 80 GET /test.php HTTP/1.1 Host: qgy18.imququ.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也可以看到类似结果,有兴趣的同学可以自己试一下。

阅读(1346) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~