本来啊,本来,本来我在准备完善这个鸽了四年的系列的时候,是打算按照时间的顺序来完成的,好吧。我承认那个时候考虑的稍稍稍稍稍微有些不足,就是我忽略了HTTP协议的“模块性“。因为虽然按照时间顺序写写流水账好像是个不错的选择,但是写着写着发现,其实HTTP的头字段,往往是一块一块的,这一块的部分包含了某一系列的字段作为请求和应答的协商方式。
那问题来了,按照时间顺序会把”模块“打散,思路实在是有些混乱,但是完全按照模块的形式又忽略了HTTP历程的发展。所以,我思前想后,辗转反侧,决定以模块为核心,辅之以该模块的历史进程,这样总可以了吧。
然后呢,这里还有个意料之外的事情,就是……,如果完全按照时间顺序来写,就有个很无奈的头重脚轻的问题,HTTP的发展并不是缓慢的、平均的、顺序的发展,0.9及其之前的部分只是个论文,0.9和1.0的部分只是个memo,到了1.1才算是可以广泛的应用。然后再继续往后的2.0、3.0酱紫发展。所以,还真不能完全按照时间顺序来写流水账。嗯……好在我悬崖勒马,及时意识到了这个问题。知耻而后勇,嗯……就这样。
以下是正文。
上一章,我们聊完了HTTP的特点和起始行的部分,并且着重的聊了聊请求方法和状态码。这两个东西十分重要,因为它们往往会配合头字段使用,我一再强调,后续的内容在涉及到相关内容的时候。从这一章开始,直到HTTP/2为止,我会带大家学习并通过Node来实践HTTP/1的核心头字段部分,HTTP的一些能力,其实大部分都是通过头字段来扩展的。
那么这一章,我们就来学一学跟body有关的头字段部分。
我们先来回忆一下,关于body的部分,到目前为止,我们已知的内容有哪些呢?在0.9的时代,可以说是只有响应返回的body的,而没有请求的body。到了1.0才有了请求体和响应体,也就是请求和响应才双双有了body,到了1.1则扩展了一些关于body的字段。我们下面就来看看关于body的头字段的内容及其协商的方式。
一、MIME
我之前简单的聊到过这个东西,想必大家有点印象,MIME在HTTP的body体系中发挥了十分重要的作用。我们需要深入的了解下。我之前说过,当我们需要传递一些数据或者内容的时候,我怎么才能把数据传递给对方并且正确的翻译这份数据呢?传递的事情交给TCP我们不做太多的深究,而翻译的工作则是由沟通双方,或者说客户端和服务器来做。
客户端传给服务器一个图片,服务器怎么知道这是个图片呢?或者反过来,客户端怎么知道这是个图片呢?理论上讲,无论用什么办法都不行。除非,我把”这是个图片“告诉你。是不是感觉有点简单,说白了就是协商。甚至于服务器收到了客户端的消息,知道了这是一个图片,但是我就是不按照图片来解析,直接给你报个错,你也没办法。
但是“标准”的意义就是我们要按照标准来,所以……虽然服务器可以不按规矩来,但是我们得按照规矩来学。
我们继续,哈哈哈,MIME是啥呢?MIME的全称叫做多用途互联网邮件扩展(Multipurpose Internet Mail Extensions),它本来是用在电子邮件系统里的,是为了可以让邮件发送多类型的数据,在这里有比较详细的介绍,大家有兴趣可以自己看。当HTTP也需要这个东西的时候,就发现,欸?MIME不错,我可以直接拿来用,省着我再自己搞一套了,所以HTTP顺手牵羊就拿过来一部分用了。
MIME把数据分为了八大类,格式差不多是这样的:type/subtype。它的八大类型差不多有这些:
- Text:用于标准化地表示的文字讯息,文字讯息可以是多种字符集和或者多种格式的;
- Multipart:用于连接消息体的多个部分构成一个消息,这些部分可以是不同类型的资料;
- Application:用于传输应用程序资料或者二进制资料;
- Message:用于包装一个E-mail讯息;
- Image:用于传输静态图片资料;
- Audio:用于传输音频或者音声资料;
- Video:用于传输动态影像资料,可以是与音频编辑在一起的视讯资料格式;
- Font:用于传输字体文件;
- Model:用于传输3D模型文件。
大家对其中的一些类型是不是都比较熟悉,比如Text、Multipart、Application、Image、Video等等,咱们在实际的工作中肯定都或多多少的接触过。然后,我们再来看看子类型有哪些:
- text/plain(纯文字)
- text/html(HTML文件)
- application/xhtml+xml(XHTML文件)
- image/gif(GIF图片)
- image/jpeg(JPEG图片)
- image/png(PNG图片)
- audio/mpeg(MP3音频)
- audio/aac(AAC音频)
- video/mpeg(MPEG视频)
- video/mp4(MPEG-4视频)
- application/octet-stream(任意的二进制数据)
- application/json(JSON文件)
- application/pdf(PDF文件)
- application/msword(Microsoft Word文件)
- application/vnd.openxmlformats-officedocument.wordprocessingml.document(Microsoft Word 2007文件)
- application/vnd.wap.xhtml+xml (wap1.0+)
- application/xhtml+xml (wap2.0+)
- message/rfc822(RFC 822形式)
- multipart/alternative(HTML邮件的HTML形式和纯文本形式,相同内容使用不同形式表示)
- application/x-www-form-urlencoded(使用HTTP的POST方法送出的表单)
- multipart/form-data(同上,但主要用于表单送出时伴随文件上传的场合)
我列出了大多数的数据类型,以及其子类型,当然,这些东西并不一定要求大家都完全理解,见名知意即可。而我加粗字体的部分,其实就是我们日常工作中最常见的几种数据类型。
二、数据类型
在HTTP中,我们可以通过Accept字段来告知服务器希望接收什么类型的数据,服务器则用Content头字段来告知客户端实际发送了什么数据。注意Accept和Content是一个分类,也是我们本章要聊的核心内容。其中包含了不少的头字段,我们也会慢慢说。
我们继续说回来这个表示数据类型的头字段,Accept字段会表示客户端可以理解的MIME type,可以用“,”分割,列出多个类型,让服务器有更多选择的可能,比如:
Accept: application/json,text/html,application/xml
这就是告诉服务器,我能解析的数据类型有json、html以及xml,可以给我这些类型范围内的数据。
相应的,服务器会使用Content-Type头字段告知客户端实体数据的真实类型:
Content-Type: application/json
这样浏览器读取Content-Type就知道是个json文件,然后通过引擎解析,就完事了。
很简单对吧。
然后……我还是要强调一下,如果服务器收到了客户端想要的数据类型,但是我就不按照你想要的给你,咋滴,那其实也一点问题没有,所以,在早期的RFC1945中,Accept是附加在其他功能中的。直到1.1的时候,才正式加入标准。
三、数据压缩
通常情况下,我们在传输数据的时候,为了可以更好的节省带宽,都会对数据进行压缩后再传输。在HTTP中也是如此,那压缩数据的方式往往有很多种,当然,这个很多就比MIME要少很多了,只有三种:
- gzip:熟悉吧,也就是GNU zip压缩格式,也是互联网上最最最流行的压缩格式;
- deflate:zlib(deflate)压缩格式,流行程度仅次于 gzip;
- br:一种专门为 HTTP 优化的新压缩算法(Brotli)。
那么客户端就可以使用Accept-Encoding字段来标记支持的压缩格式,也可以通过“,”来分割多个支持的格式,服务器则会把实际使用的压缩格式放到Content-Encoding字段里。
Accept-Encoding: gzip, deflate, br Content-Encoding: gzip
在实际使用中,这两个字段是可以省略的,客户端省略意味着不支持压缩,服务器的省略则是告知客户端传输的这份数据没有被压缩。
四、语言类型
有了数据类型和压缩类型,可以让机器识别出传输的数据是什么以及如何解压了。但是全球各地有这么多的国家和地区,不同的国家和地区都使用不同的语言,甚至是相同国家和地区的人都可能使用不同的语言,那浏览器怎么显示出每个人都可以理解的语言文字呢?换句话说,就是我如何根据不同的情况来正确的编码这份数据呢?再换句话说,其实就是国际化的问题。
我猜已经学到了这里的你已经知道怎么解决了,协商呗,字段呗。哈哈哈,感觉有点无聊。。一点悬念都没有。
对于请求头来说使用的字段是Accept-Language,对于响应报文中的实体头字段则是Content-Language,这里大家要注意一点,Accept头字段是请求头字段,而Content则是实体头字段,不是响应头字段噢。这个大家要注意一下。
例子如下:
Accept-Language: zh-CN, zh, en Content-Language: zh-CN
很简单,也不复杂,但是还没完,这些头字段对应的值是什么东东?嗯……这些东西叫做语言类型,就是 人类使用的自然语言,比如英语、汉语、法语等等,而这些自然语言也有其下属方言,所以跟数据类型类似,也是type-subtype的形式,而与语言类型不同的是。数据类型是用”/”来分割父类与子类,语言类型则是用“-”来分割。
比如,en表示英语,en-US表示美式英语,en-GB表示英式英语,当然还有更多的语言类型,大家可以自行了解下,这里多说无益。
到了这里,服务器知道了用什么类型的语言,但是你要知道计算机的底层本质就是0和1,我要怎么把0和1翻译成对应的语言呢?这就要用到字符集了,在计算机发展的早期,十分的混乱,各个国家和地区的人们都自己定义了一套体系,发明了许多编码来处理各自的文字,比如英语用ASCII,汉语用GBK。这就导致同样一段文字,用不同的编码就可能会显示的一点都不一样。
所以后来就出现了Unicode和UTF-8,把世界上所有的语言都容纳在一种方案里。
在HTTP中的请求头中,可以通过Accept-Charset来表达客户端可以接受的编码类型,但是响应头里却没有对应的字段,而是在Content-Type字段的数据类型后面用“charset=xxx”来表示,这点你要特别注意。
Accept-Charset: gbk, utf-8 Content-Type: text/html; charset=utf-8
不过在现代的浏览器里都支持多种字符集,所以通常情况下,不会发送Accept-Charset请求头,服务器也不会返回Content-Language,因为使用的语言完全可以通过字符集推断出来,所以一般在请求头里只有Accent-Language,响应头里只有Content-Type。
五、质量值
质量值的英文名叫做quality factory,直译过来叫做质量因数,其实就是权重的意思啦。它在HTTP中使用q作为一个参数,形式就是“q=value”,这个value可以是0到1之间,包含0和1的两位小数。与字段中的值用“;”来分割。
这里要强调一点的是,在其它大多数语言中,就比如JavaScript吧,分号“;”的断句语气是要强于逗号“,”的,但是在HTTP中则相反。我们看个例子:
Accept: text/html,application/xml;q=0.9,*/*;q=0.8
这段话是啥意思呢,就是浏览器最希望服务器传过来的是html文件,不写默认权重就是1,其次是xml文件,权重是0.9,最后就是权重为0.8的任意文件类型。服务器收到请求后,就会根据这段内容,来优先返回HTML。
六、Vary
这个东西有点怪怪的,我们来学学。它的意思是,我返回给你的响应报文,参考了哪些头字段。也就是说,客户端与浏览器在协商确定响应报文该如何返回的过程,其实并不透明,你不知道是咋协商的,或者服务器根本就不管你协商不协商都是有可能的。
但是友好一点的服务器会在响应头里多加一个Vary字段,记录服务器在内容协商时参考的请求头字段,给出一点信息。
Vary: Accept-Encoding,User-Agent,Accept
上面的例子表示服务器参考了Accept-Encoding,User-Agent,Accept这三个字段后,返回了响应报文。
Vary 字段可以认为是响应报文的一个特殊的“版本标记”。每当 Accept 等请求头变化时,Vary 也会随着响应报文一起变化。也就是说,同一个 URI 可能会有多个不同的“版本”,主要用在传输链路中间的代理服务器实现缓存服务,这个之后讲“HTTP 缓存”时还会再提到。
七、分块传输
我们前六个小节,聊了聊数据是如何在HTTP中协商才可以让客户端与服务器双方知道怎么处理该数据。并且如果数据体积过大,我们还可以通过协商压缩方式来给传输的数据进行压缩传输。看起来,好像一切都挺美好的,但是如果我要传输的文件体积特别大呢?比如一个视频……小的有几百兆,大的有几个G,并且针对视频的压缩效率是很低的,那你怎么传输呢?
嗯……标题就是答案。我们没办法把一个大体积的数据整体变小,那么我们只能把这个特别大的数据进行切分,切分成一小块一小块的,服务器把这些小块的数据传输给浏览器,浏览器收到后再按照一定的规则组装复原。
这种思路在HTTP中就叫做chunked,也就是分块传输编码,在响应报文里可以使用“Transfer-Encoding: chunked”来表示,意思就是响应报文中的body不是一次性发送的,而是分成了许多的块逐次发送的。
大家要注意的一点是,一个响应报文的长度要么已知,要么未知,不可能即知又不知,什么意思呢?就是Transfer-Encoding: chunked和Content-length是互斥的,不能同时出现在响应头里。
八、范围请求
有了分块传输,我们可以把一份体积庞大的数据逐一发送,解决大文件在传输过程中的卡死问题。我们还是拿视频来举例,你在腾讯视频或者爱奇艺上看电视剧,看的正开心呢,突然弹出来一个视频内广告,或者你不想看电视剧的开头和结尾,你会通过拖动进度条来跳过这部分内容,那这中实现就需要用到范围请求。
换句话说,我们希望可以获取一个大文件的某一块片段,而分块传输是做不到这点的,分块传输只能在开始传输的时候就把块分好传给你,无法确定我需要的某一个范围的数据。
解决这样的问题就需要用到范围请求了,范围请求允许客户端在请求头里使用专用字段来表示只获取整个文件的一部分。
范围请求并不是Web服务器必备的功能,可以实现也可以不实现,所以服务器必须在响应头里使用字段“Accept-Ranges: bytes”明确的告知客户端我是支持范围请求的。如果不支持的话可以用“Accept-Ranges: none”告知客户端,或者直接就不发送Accept-Ranges字段。
客户端使用“Range”作为范围请求的请求头格式是“bytes=x-y”,x和y就是以字节为单位的范围数据了,x必须从0开始,比如0-9是指前10个字节,以此类推。
当服务器收到Range字段后,就会做四件事:
- 首先服务器会检查你传过来的Range范围是否合法,不合法的范围服务器会直接甩给你一个416,告诉你请求的数据范围是不合法的。
- 其次,如果范围合法,那么服务器则会根据你的范围读取文件的片段,返回206状态码,也就是Partial Content,表示返回了原数据的一部分。
- 服务器在返回部分数据的时候会加上一个Content-Range响应头,告诉客户端实际的偏移量和资源的总大小,格式是这样的“bytes x-y/length”。
- 最后就是发送数据了。
不仅仅是看视频拖拽进度可以用到范围请求,下载时候的多端下载和断点续传,实际上也是基于它来实现的,这个我们下一章实践的时候再说。
九、多段数据
基于范围请求,我们还可以请求不止一个范围的片段,也就是一次性请求多段数据。这种情况需要用到一个特殊的MIME类型:multipart/byterange,表示报文的body是由多段字节序列组成的,并且还需要一个boundary=xxx来给出段之间的分割标记。就像这样:
Content-Type: multipart/byteranges; boundary=00000000001
这个boundary=00000000001就是分割标记,开始以–00000000001开始,–00000000001–结束。
嗯……这么说理论肯定有点模糊,我们下一篇动手实践的时候就可以清楚的看到它的形式是什么样子的了。
总结
本篇我们主要聊了两件事,一个是文件、另外一个就是传输文件。文件相关的我们聊了文件的类型、语言类型、简单压缩等等,传输文件则主要有两个部分,一个是分块传输、一个是分段传输。就这点东西,没了,全是理论。下一篇,我们就本篇的内容,来把我们学过的这些理论全都实践一遍,加深印象。
另外,我还要强调一下第四部分聊的语言类型和国际化的问题,实际上在HTTP中的国际化,是指你传输的文件内的数据语言,并不是我们在前端单页应用中使用的国际化插件,这两者是有差别的。