上次我们谈到了HTTP报文里的div,知道了HTTP可以传输很多种类的数据,不仅是文本,也能传输图片,音频和视频。
早期互联网上传输的基本上都是只有几k大小的文本和小图片,现在的情况则大有不同。网页里包含的信息实在太多了,随随便便一个主页HTML就有可能上百K,高质量的图片都以M论,更不要说那些电影,电视剧了,几G,几十G都有可能。
相比之下,100M的光纤固网或者4G移动网络在这些大文件的压力下都变成了小水管,无论是上传还是下载,都会把网络传输链路挤的满满当当。
所以如何在有限的带宽下高效快捷的传输这些大文件就成了一个重要的课题,这就好比是已经打开了冰箱门,该怎么把大象塞进去在关上门呢?
下面我们就一起看看HTTP协议里有哪些手段能解决这个问题。
数据压缩
通常浏览器在发送请求时都会带着Accept-Encoding头字段,里面是浏览器支持的压缩格式,例如gzip,deflate等,这样服务器就可以从中选择一种压缩算法,放进Content-Encoding响应头里,在把原数据压缩后发给浏览器。
如果压缩率能有50%,也就是说100k的数据能够压缩成50k的大小,那么就相当于在带宽不变的情况下网速提升了一倍,加速的效果是非常明显的。
不过这个解决方法也有个缺点,gzip等压缩算法通常只对文本文件有较好的压缩率,而图片,音频视频等多媒体数据本身已经是高度压缩的,在用gzip处理也不会变小,所以它就失效了。
不过数据压缩在处理文本的时候效果还是很好的,所以各大网站的服务器都会使用这个手段作为保底,例如,在nginx里就会使用gzip on指令,启用对text/html的压缩。
分块传输
在数据压缩之外,还能有什么办法来解决大文件的问题呢?
压缩是把大文件整体变小,我们可以反过来思考,如果大文件整体不能变小,那就把它拆开,分解成多个小块,把这些小块分批发给浏览器,浏览器收到后在组装复原。
这样浏览器和服务器都不用在内存里保存文件的全部,每次只收发一小部分,网络也不会被大文件长时间占用,内存,带宽等资源也就节省下来了。
这种化整为零的思路在HTTP协议里就是chunked分块传输编码,在响应报文里用头字段Transfer-Encoding:chunked来表示,意思是报文里的div部分不是一次性发过来的,而是分成了许多的块(chunk)逐个发送。
分块传输也可以用于流式数据,例如有数据库动态生成的表单页面,这种情况下div数据的长度是未知的,无法在头字段Content-Length给出确切的长度,所以也只能用chunked方式分块发送。
Transfer-Encoding:chunked和Content-Length这两个字段是互斥的,也就是说响应报文里这两个字段不能同时出现,一个响应报文里的传输要么是长度已知,要么是长度未知。
下面是分块传输的编码规则:
1.每个分块包含两个部分,长度头和数据块
2.长度头是以CRLF结尾的一行明文,用16进制数字表示长度
3.数据块紧跟在长度头后,最后也用CRLF结尾,但数据不包含CRLF
4.最后用一个长度为0的块表示结束
范围请求
有了分块传输编码,服务器就可以轻松收发大文件了,但对于上G的超大文件,还有一些问题需要考虑。
比如,你在看当下正热播的电视剧,想跳过片头,直接看正片,或者有段剧情无聊,想拖动进度条快进几分钟,这实际上是想获取一个大文件其中的片段数据,而分块传输并没有这个能力。
HTTP协议为了满足这样的需求,提出了范围请求的概念,允许客户端在请求头里使用专用字段来表示只获取文件的一部分,相当于是客户端的化整为零。
范围请求不是web服务器必备的功能,可以实现也可以不实现,所以服务器必须在响应头里使用字段Accept-Ranges:bytes明确告知客户端:我是支持范围请求的。
请求头Range是HTTP范围请求的专用字段,格式是"bytes=x-y",其中的x和y是以字节为单位的数据范围。
要注意x,y表示的是偏移量,范围必须从0计数,例如前10个字节表示为0-9,第二个10字节表示为10-19,而0-10实际上是前11个字节。
Range的格式也很灵活,起点x和终点y可以不要,能够很方便的表示正数或者倒数的范围。假设文件是100个字节,那么:
"0-"表示从文档起点到文档终点,相当于"0-99",即整个文件
“10-”是从第10个字节开始到文档末尾,相当于"10-99"
"-1"是文档的最后一个字节,相当于"99-99"
"-10"是从文档末尾倒数10个字节,相当于"90-99"
服务器收到Range字段后,需要做四件事
第一,它必须检查范围是否合法,比如文件只有100个字节,但是请求"200-300",这就是范围越界了,服务器就会返回状态码416,意思是你的范围请求有误,我无法处理,请在检查一下
第二,如果范围正确,服务器就可以根据Range头计算偏移量,读取文件的片段了,返回状态码206 partial content,和200的意思差不多,但表示div只是原数据的一部分
第三,服务器要添加一个响应头字段Content-Range,告诉片段的实际偏移量和资源的总大小,格式是bytes x-y/length,与Range头区别在没有“=”,范围后多了总长度
最后剩下的就是发送数据了,直接把片段用TCP发送给客户端,一个范围请求就算是处理完了。
多段数据
刚才说的范围请求一次只获取一个片段,其实它还支持在Range头里使用多个"x-y",一次性获取多个片段数据。
这种情况需要使用一种特殊的MIME类型:multipart/byteranges,表示报文的div是由多段字节序列组成的,并且还要用一个参数boundary=xxx给出段之间的分隔标记。
每一个分段必须以"--boundary"开始,之后要用Content-Type和Content-Range标记这段数据的类型和所在范围,然后就像普通的响应头一样以回车换行结束,在加上分段数据,最后用一个"--boundary--"表示所有的分段结束。