前端学HTTP之内容协商
前面的话
一个URL常常需要代表若干不同的资源。例如那种需要以多种语言提供其内容的网站站点。如果某个站点有说法语的和说英语的两种用户,它可能想用这两种语言提供网站站点信息。理想情况下,服务器应当向英语用户发送英文版,向法语用户发送法文版——用户只要访问网站主页就可以得到相应语言的内容
HTTP提供了内容协商方法,允许客户端和服务器作这样的决定。通过这些方法,单一的URL就可以代表不同的资源(比如,同一个网站页面的法语版和英语版),这些不同的版本称为变体。本文将详细介绍内容协商
总括
对于特定的URL来说,服务器可以根据一些原则来决定发送什么内容给客户端最合适。在有些场合下,服务器甚至可以自动生成定制的页面。比如,服务器可以为手持设备把HTML页面转换成WML页面。这类动态内容变换被称为转码。这些变换动作是HTTP客户端和服务器之间进行内容协商的结果
共有3种不同的方法可以决定服务器上哪个页面最适合客户端:让客户端来选择、服务器自动判定,或让中间代理来选。这3种技术分别称为客户端驱动的协商、服务器驱动的协商以及透明协商
客户端驱动
对于服务器来说,收到客户端请求时只是发回响应,在其中列出可用的页面,让客户端决定要看哪个,这是最容易的事情。很显然,这是服务器最容易实现的方式,而且客户端很可能选择到最佳的版本(只要列表中有让客户端选择的足够信息)。不利之处是每个页面都需要两次请求:第一次获取列表,第二次获取选择的副本。这种技术速度很慢且过程枯燥乏味,让用户厌烦
从实现原理上来说,服务器实际上有两种方法为客户端提供选项:一是发送回一个HTML文档,里面有到该页面的各种版本的链接和每个版本的描述信息,另一种方法是发送回HTTP/1.1响应时,使用300 Multiple Choices响应代码。客户端浏览器收到这种响应时,在前一种情况下,会显示一个带有链接的页面,在后一种情况下,可能会弹出对话窗口,让用户做选择。不管怎么样,决定是由客户端的浏览器用户作出的
除了增加时延并且对每个页面都要进行繁琐的多次请求之外,这种方法还有一个缺点:它需要多个URL:公共页面要一个,其他每种特殊页面也都要一个。因此,比如说原始的请求地址是www.joes-hardware.com,Joe的服务器可能会回复某个页面,该页面里面有到www.joes-hardware.com/english和www.joes-hardware.com/french的链接。如果客户端想加书签的话,是要加在原始的公共页面上呢,还是加在选中的页面上呢?如果用户想把这个网站推荐给他的朋友,是告知www.joes-hardware.com这个地址好呢,还是只告诉他们讲英语的朋友www.joes-hardware.com/english这个地址?
服务器驱动
减少额外通信量的一种方法是让服务器来决定发送哪个页面回去,但为了做到这一点,客户端必须发送有关客户偏好的足够信息,以便服务器能够作出准确的决策。服务器通过客户端请求的首部集来获得这方面的信息
有以下两种机制可供HTTP服务器评估发送什么响应给客户端比较合适
1、检査内容协商首部集。服务器察看客户端发送的Accept首部集,设法用相应的响应首部与之匹配
2、根据其他(非内容协商)首部进行变通。例如,服务器可以根据客户端发送的User-Agent首部来发送响应
【内容协商首部集】
客户端可以用下面列出的HTTP首部集发送用户的偏好信息
首部 描述 Accept 告知服务器发送何种媒体类型 Accept-Language 告知服务器发送何种语言 Accept-Charset 告知服务器发送何种字符集 Accept-Encoding 告知服务器采用何种编码
[注意]这些首部与实体首部非常类似。不过,这两种首部的用途截然不同。实体首部集像运输标签,它们描述了把报文从服务器传输给客户端的过程中必须的各种报文主体属性。而内容协商首部集是由客户端发送给服务器用于交换偏好信息的,以便服务器可以从文档的不同版本中选择出最符合客户端偏好的那个来提供服务
服务器用下面列出的实体首部集来匹配客户端的Accept首部集
Accept首部 实体首部 Accept Content-Type Accept-Language Content-Language Accept-Charset Content-Type Accept-Encoding Content-Encoding
由于HTTP是无状态的协议,表示服务器不会在不同的请求之间追踪客户端的偏好,所以客户端必须在每个请求中都发送其偏好信息
如果两个客户端都发送了Accept-Language首部,描述它们感兴趣的语言信息,服务器就能够决定发送www.joes-hardware.com的何种版本给哪个客户端了。让服务器自动选择发送回去的文档,减少了往返通信的时延,这种时延是客户端驱动模型中无法避免的
然而,假设某个客户端偏好西班牙文,那服务器应当回送哪个版本的页面呢?英语还是法语?服务器只有两种选择:猜测或回退到客户端驱动模型,问客户端选择哪个。假如这个西班牙人碰巧懂一点英语,他可能会选择英文页面,这不是最理想的,但它能解决问题。在这种情况下,这个西班牙人需要有办法传达更多与其偏好有关的信息,也就是他的确对英语略知一二,在没有西班牙语的时候,英语也行
幸运的是,HTTP提供了一种机制,可以让与这个西班牙人情况类似的客户端更详细地描述其偏好。这种机制就是质量值(简称q值)
HTTP协议中定义了质量值,允许客户端为每种偏好类别列出多种选项,并为每种偏好选项关联一个优先次序。例如,客户端可以发送下列形式的Accept-Language首部:
Accept-Language: en; q=0.5, fr; q=0.0 , nl; q=1.0, tr; q=0.0
其中q值的范围从0.0-1.0(0.0是优先级最低的,而1.0是优先级最高的)。上面列出的那个首部,说明该客户端最愿意接收荷兰语(缩写为nl)文档,但英语(缩写为en)文档也行;无论如何,这个客户端都不愿意收到法语(缩写为fr)或土耳 其语(缩写为tr)的版本
[注意]偏好的排列顺序并不重要,只有与偏好相关的q值才是重要的
服务器偶尔也会碰到找不到文档可以匹配客户端的任何偏好的情况。对于这种情况,服务器可以修改文档,也就是对文档进行转码,以匹配客户端的偏好
【其他首部集】
服务器也可以根据其他客户端请求首部集来匹配响应,比如User-Agent首部。例如,服务器知道老版本的浏览器不支持JavaScript语言,这样就可以向其发送不含有JavaScript的页面版本
在这种情况下,没有q值机制可供査找“最近似”的匹配。服务器或者去找完全匹配,或者简单地有什么就给什么,这取决于服务器的实现
由于缓存需要尽力提供所缓存文档中正确的“最佳”版本,HTTP协议定义了服务器在响应中发送的Vary首部。这个首部告知缓存,还有客户端和所有下游的代理,服务器根据哪些首部来决定发送响应的最佳版本
【Apache】
下面概括了著名的Web服务器Apache是如何支持内容协商的。网站的内容提供者,比如说Joe要负责为Joe的索引页面提供不同的版本。Joe还必须把这些索引页面文件放在和站点相关的Apache服务器的适当目录下。用以下两种方式可以启用内容协商
1、在网站目录中,为网站中每个有变体的URI创建一个type-map(类型映射)文件。这个type-map文件列出了每个变体和其相关的内容协商首部集
2、启用MultiViews指令,这样会使Apache自动为目录创建type-map文件
【使用type-map文件】
Apache服务器需要知道type-map文件的命名规则。可以在服务器的配置文件中设置handler来说明type-map文件的后缀名。例如:
AddHandler type-map .var
这行就说明了后缀是.var的文件就是type-map文件
下面给出一个type-map文件示例
根据这个type-map文件,Apache服务器就知道要发送joes-hardware.en.html给请求英语版的客户端,发送joes-hardware.fr.de.html给请求法语版的客户端。Apache服务器也支持质量值
【使用MultiView】
为了使用MultiView,必须在网站目录下的access.conf文件中的适当小节(<Directory>、<Location>,或<Files>)使用OPTION指令来启用它
如果启用了MultiView,而浏览器又请求了名为joes-hardware的资源,服务器就会査找所有名字中含有joes-hardware的文件,并为它们创建type-map文件。服务器会根据名字猜测其对应的内容协商首部集。例如,法语版的joes-hardware应当含有.fr
另一种在服务器端实现内容协商的方法是使用服务器端扩展,比如微软的动态服务器页面(Microsoft’s Active Server Pages, ASP)
透明协商
透明协商机制试图从服务器上去除服务器驱动协商所需的负载,并用中间代理来代表客户端以使与客户端的报文交换最小化。假定代理了解客户端的预期,这样就可以代表客户端与服务器协商,在客户端请求内容的时候,代理已经收到了客户端的预期
为了支持透明内容协商,服务器必须有能力告知代理,服务器需要检査哪些请求首部,以便对客户端的请求进行最佳匹配。HTTP/1.1规范中没有定义任何透明协商机制,但定义了Vary首部。服务器在响应中发送了Vary首部,以告知中间节点需要使用哪些请求首部进行内容协商
代理缓存可以为通过单个URL访问的文档保存不同的副本。如果服务器把它们的决策过程传给缓存,这些代理就能代表服务器与客户端进行协商。缓存同时也是进行内容转码的好地方,因为部署在缓存里的通用转码器能对任意服务器,而不仅仅是一台服务器传来的内容进行转码
【缓存与备用候选】
对内容进行缓存的时候是假设内容以后还可以重用。然而,为了确保对客户端请求回送的是正确的已缓存响应,缓存必须应用服务器在回送响应时所用到的大部分决策逻辑
上面描述了客户端发送的Accept首部集,以及为了给每条请求选择最佳的响应,服务器使用的与这些首部集匹配的相应实体首部集。缓存也必须使用相同的首部集来决定回送哪个已缓存的响应
下图展示了涉及缓存的正确及错误的操作序列。缓存把第一个请求转发给服务器,并存储其响应。对于第二个请求,缓存根据URL査找到了匹配的文档。但是,这份文档是法语版的,而请求者想要的是西班牙语版的。如果缓存只是把文档的法语版本发给请求者的话,它就犯了错误
因此,缓存也应该把第二条请求转发给服务器,并保存该URL的响应与“备用候选”响应。缓存现在就保存了同一个URL的两份不同的文档,与服务器上一样。这些不同的版本称为变体(variant)或备用候选(alternate)。内容协商可看成是为客户端请求选择最合适变体的过程
【Vary 首部】
这里是浏览器和服务器发送的一些典型的请求及响应首部
然而,如果服务器的决策不是依据Accept首部集,而是比如User-Agent首部的话,情况会如何?这不像听起来这么极端。例如,服务器可能知道老版本的浏览器不支持JavaScript语言,因此可能会回送不包含JavaScript的页面版本。如果服务器是根据其他首部来决定发送哪个页面的话,缓存必须知道这些首部是什么,这样才能在选择回送的页面时做出同样的逻辑判断
HTTP的Vary响应首部中列出了所有客户端请求首部,服务器可用这些首部来选择文档或产生定制的内容(在常规的内容协商首部集之外的内容)。例如,若所提供的文档取决于User-Agent首部,Vary首部就必须包含User-Agent
当新的请求到达时,缓存会根据内容协商首部集来寻找最佳匹配。但在把文档提供给客户端之前,它必须检査服务器有没有在已缓存响应中发送Vary首部。如果有Vary首部,那么新请求中那些首部的值必须与旧的已缓存请求里相应的首部相同。因为服务器可能会根据客户端请求的首部来改变响应,为了实现透明协商,缓存必须为每个已缓存变体保存客户端请求首部和相应的服务器响应首部,参见下图
如果某服务器的Vary首部看起来像下面这样,大量不同的User-Agent和Cookie值将会产生非常多的变体:
Vary: User-Agent, Cookie
缓存必须为每个变体保存其相应的文档版本。当缓存执行査找时,首先会对内容协商首部集进行内容匹配,然后比较请求的变体与缓存的变体。如果无法匹配,缓存就从原始服务器获取文档
转码
我们已经讨论了一个机制,该机制可以让客户端和服务器从某个URL的一系列文档中挑选出最适合客户端的文档。实现这些机制的前提是,存在一些满足客户端需求的文档——不管是完全满足还是在一定程度上满足
然而,如果服务器没有能满足客户端需求的文档会怎么样呢?服务器可以给出一个错误响应。但理论上,服务器可以把现存的文档转换成某种客户端可用的文档。这种选项称为转码
下面列出了一些假设的转码
转换之前 转换之后
HTML文档 WML文档
高分辨率图像 低分辨率图像
彩色图像 黑白图像
有多个框架的复杂页面 没有很多框架或图像的简单文本页面
有Java小应用程序的HTML页面 没有Java小应用程序的HTML页面
有广告的页面 去除广告的页面
有3种类别的转码:格式转换、信息综合以及内容注入
【格式转换】
格式转换是指将数据从一种格式转换成另一种格式,使之可以被客户端査看。通过HTML到WML的转换,无线设备就可以访问通常供桌面客户端査看的文档了。通过慢速连接访问Web页面的客户端并不需要接收高分辨率图像,如果通过格式转换降低图像分辨率和颜色来减小图像文件大小的话,这类客户端就能更容易地査看图像比较丰富的页面了
格式转换可以由内容协商首部集来驱动,但也能由User-Agent首部来驱动。注意,内容转换或转码与内容编码或传输编码是不同的,后两者一般用于更高效或安全地传输内容,而前两者则可使访问设备能够査看内容
【信息综合】
从文档中提取关键的信息片段称为信息综合(information synthesis),这是一种有用的转码操作。这种操作的例子包括根据小节标题生成文档的大纲,或者从页面中删除广告和商标
根据内容中的关键字对页面分类是更精细的技术,有助于总结文档的精髓。这种技术常用于Web页面分类系统中,比如门户网站的Web页面目录
【内容注入】
前面描述的两类转码通常会减少Web文档的内容,但还有另一类转换会增加文档的内容,即内容注入转码。内容注入转码的例子有自动广告生成器和用户追踪系统
设想一下,一个能往途经的每个HTML页面中自动添加广告的广告植入转码器是多么的诱人,当然也很烦人。这类转码操作只能动态进行——它必须即时添加与当前的特定用户有关,或针对特定用户的广告。也可以构建用户追踪系统,在页面中动态增加内容,用于收集用户査看页面和客户端浏览方式的统计信息
【转码与静态预生成的对比】
转码的替代做法是在Web服务器上建立Web页面的不同副本,例如一个是HTML,一个是WML;一个图像分辨率高,一个图像分辨率低;一个有多媒体内容,一个没有。但是,这种方法不是很切合实际,原因很多:某个页面中的任何小改动都会牵扯很多页面,需要很多空间来存储各页面的不同版本,而且使页面编目和Web服务器编程(以提供正确的版本)变得更加困难。有些转码操作,比如广告插入(尤其是定向广告插入),就不能静态实现——因为插入什么广告和请求页面的用户有关
对单一的根页面进行即时转换,是比静态的预生成更容易的解决方案。但这样会在提供内容时增加时延。不过有时候其中一些计算可以由第三方进行,这样就减少了Web服务器上的计算负荷——比如可以由代理或缓存中的外部Agent完成转换
下图显示了在代理缓存中进行的转码
好的代码像粥一样,都是用时间熬出来的