1.3 HTTP的语法和历史
1989年,在CERN,Tim Berners-Lee和他的团队启动了对HTTP的研究。该研究旨在实现一种计算机互联网络,并通过此网络提供对研究成果的访问,且将研究链接起来,以便它们可以方便即时地相互引用——单击一个链接就可以打开相关的文档。此类系统的构想已经存在相当长一段时间,超文本一词也于20世纪60年代问世。随着20世纪80年代因特网的发展,实现此构想成为可能。在1989年和1990年间,Berners-Lee发布了构建此类系统的提议[6]。紧接着他构建了第一个基于HTTP的Web服务器,以及第一个Web浏览器,用以请求并显示HTML文档。
1.3.1 HTTP/0.9
HTTP的第一个规范是1991年发布的0.9版本。此规范文档[7]只有不到700个单词。该规范指定,通过TCP/IP(或类似的面向连接的服务)与服务器和端口(可选的,如果未指定端口,则使用80)建立连接。客户端应发送一行ASCII文本,包括GET、文档地址(无空格)、回车符和换行符(回车是可选的)。服务器使用HTML格式的消息进行响应,该消息被定义为“ASCII字符的字节流”。规范还指定,“通过服务器关闭连接来终止消息”,这就是为什么在前面的示例中,在每个请求之后都关闭了连接。在处理错误时,规范声明:“错误响应以可读的文本显示,使用HTML语法。除了文本的内容,没有办法区分错误响应和正确响应。”在文档结尾处,规范指出:“请求是幂等的。服务器不需要在断开连接后存储关于请求的任何信息。”本规范为我们提供了HTTP的无状态特性,这是一把双刃剑,有利(这很简单)也有弊(因为必须附加HTTP cookies等技术以允许状态跟踪,这对于复杂的应用程序是必需的)。
如下是HTTP/0.9可能仅有的指令:
当然,请求的资源(/section/page.html)可以改变,但是句法中的其他部分是不变的。
当时,还没有HTTP头字段(这里称为HTTP首部)或任何其他媒体(如图像)的概念。令人惊讶的是,从这个旨在为研究机构提供对信息的便捷访问的、简单的请求/响应协议,很快催生了当今世界不可或缺又丰富多彩的万维网。即便在早期,Berners-Lee也称他的发明为WorldWideWeb(没有空格),这体现了他对于此项目的远见,以及想让其成为一个全球系统的目标。
1.3.2 HTTP/1.0
万维网如一颗新星,迅速崛起。NetCraft[8]数据显示,到1995年9月,网络上有19705个主机名。一个月后,这个数字跃升至31568,此后也一直以惊人的速度增长。在笔者写作本书时,世界上已经有接近20亿个网站。到1995年,HTTP/0.9这个简单协议的局限性已经不可忽视,大多数Web服务器都已经实现了远超0.9规范的扩展。由Dave Raggett领导的HTTP工作组(HTTP WG)开始研究HTTP/1.0,试图记录“协议的共同用法”。该文档就是RFC 1945[9],其于1996年5月发布。RFC(征求意见稿)文件由IETF发布。它可以作为正式标准被接受,也可以作为非正式文件用于存档。HTTP/1.0 RFC[10]属于后者,它并不是正式规范。在文档的开头部分,它声称自己是一个“备忘录”,并申明:“本备忘录为互联网社区提供信息。本备忘录不规定任何形式的互联网标准。”
尽管不是正式标准,HTTP/1.0还是新增了一些关键特性,包含:
• 更多的请求方法。除了先前定义的GET方法,新增了HEAD和POST方法。
• 为所有的消息添加HTTP版本号字段。此字段是可选的,为了向后兼容,默认情况下使用HTTP/0.9。
• HTTP首部。它可以与请求和响应一起发送,以提供与正在执行的请求或发送的响应相关的更多信息。
• 一个三位整数的响应状态码,(例如)用来表示响应是否成功。此状态码还可以用来表示重定向请求、条件请求和错误状态(404 - Not Found是其中最著名的错误状态之一)。
在使用协议的过程中,一些急需的扩展应运而生。并且HTTP/1.0旨在记录现实世界中多数Web服务器上已经发生的事情,而不是定义新的功能。特性的扩增给Web带来了大量的新机会。其中,通过使用HTTP响应首部来定义正文中的内容类型,人们终于可以向网页中添加多媒体内容。
HTTP/1.0的方法
GET方法与HTTP/0.9中的基本相同,但是新增的首部允许客户端发送条件GET(仅当在客户端上次请求之后,资源发生变化时,才请求资源内容;否则,告诉客户端资源没变化,继续使用旧的副本)。此外,我们之前提到过,用户可以使用GET方法获取更多的资源,而不仅仅是超文本文档,比如使用HTTP下载图像、视频或其他类型的媒体内容。
HEAD方法允许客户端获取资源的所有元信息(例如HTTP头)而无须下载资源本身。在很多场景下,此方法很有用。例如,Google的网络爬虫可以检查资源是否已被更改并仅在其改变时才下载,从而为Google和目标Web服务器节省资源。
POST方法就更有趣了,它允许客户端发送数据到Web服务器。如果Web服务器准备好接收数据并执行相应的操作,那么用户可以通过HTTP POST一个新的HTML文件到Web服务器上,而不是像标准的文件传输方法一样,将它直接放到服务器上。POST方法不局限于发送完整的文件,它可以发送更小的数据。网站上的表单通常使用POST方法发送,将表单的内容作为键值对放在HTTP请求体中发送。也就是说,POST方法允许将内容作为HTTP请求的一部分从客户端发送到服务器,这表示HTTP请求终于和HTTP响应一样,拥有了正文部分。
实际上,GET方法允许将数据包含在URL尾部指定的查询参数中发送,通常放在?字符之后。例如,https://www.google.com/?q=search+string告诉Google你正在搜索关键词search string。查询参数在最早的URI(Uniform Resource Identifier,统一资源标识符)规范[11]中定义,但它们提供了额外的参数来指明URI,而不是用它们向Web服务器上传数据。URL受到长度和内容方面的限制(例如,无法发送二进制数据),并且某些机密数据(密码、信用卡数据等)也不应出现在URL中,因为很容易就可以在屏幕和浏览器历史记录中看到这些数据。当URL被分享出去时,这些数据也包括在内。因此,POST方法通常是一种更好的数据发送方式,其中的数据也不是那么显而易见(尽管在通过透明的HTTP而不是安全的HTTPS发送时仍应小心,稍后会讨论这一点)。另一个区别是GET请求是幂等的,而POST请求不是。这意味着对于同一个URL的多个GET请求,应始终返回相同的结果;而对于同一URL请求的多个POST请求,则可能不会返回相同的结果。例如,我们刷新网站上的标准页面,它应该显示相同的内容。如果在电子商务网站刷新确认页面,则浏览器可能会询问你,是否真的要重新提交数据,这可能会导致你重复下单,尽管在编写电子商务网站应用程序时,就应该确保不会发生这种情况!
HTTP请求首部
HTTP/0.9只使用一行指令GET资源,而HTTP/1.0引入了HTTP首部(或者叫首部)。这些首部允许请求向服务器提供附加信息,可以使用首部来决定如何处理请求。HTTP首部在原始请求行之后的单独行上提供。HTTP GET请求已从:
变成了:
或者(没有首部的时候)
也就是说,在第一行中添加了一个版本部分(可选的,如果未指定,则默认为HTTP/0.9)。并且可选的HTTP首部之后跟着两个回车/换行符(以下简称为回车符)。第二行必须发送一个空行,用于表示(可选的)请求首部部分已完成。
HTTP首部使用首部名称、冒号和首部内容表示。根据规范,首部名称(而不是内容)不区分大小写。当使用空格或制表符开始新的一行时,首部可以跨越多行,但并不推荐这种做法;很少有客户端或服务器使用此格式,所以它们可能无法正确处理这种格式。可以发送具有相同名称的多个首部,在语义上这与发送以逗号分隔的版本完全相同。也就是说,
和下面这样的处理完全相同:
HTTP/1.0定义了一些标准首部,而我们这里的示例演示了在HTTP/1.0中如何提供自定义首部(在此示例中为Header1),而且无须更新版本协议。在设计协议时就考虑到了扩展性。并且,规范明确指出,这些字段不能让接收者认为是可以识别的,或许可以被忽略,然而标准首部应该由符合HTTP/1.0规范的服务器处理[12]。
一个经典的HTTP/1.0 GET请求是:
此请求告诉服务器你可以接受哪些格式的响应(HTML、XHTML和XML等),你可以接受多种编码格式(例如,gzip、deflate和brotli,这些压缩算法用于压缩通过HTTP发送的数据),你倾向于使用哪种语言(英式英语、美国英语,或者任何其他形式的英语),以及你正在使用的浏览器(MyAwesomeWebBrowser 1.1)。它还告知服务器保持连接(稍后会讲)。整个请求用两个回车符结束。从这里开始,出于可读性考虑,我们去掉了换行符。你可以假设请求中的最后一行后跟两个回车符。
HTTP响应状态码
一个经典的来自HTTP/1.0服务器的响应如下:
此处省略的内容是HTML的剩余部分。可以看到,响应的第一行包括响应消息的HTTP版本(HTTP/1.0)、三位数的HTTP状态码(200)以及该状态码的文本描述(OK)。状态码和描述是HTTP/1.0中的新概念。在HTTP/0.9中,没有响应码这样的概念,错误只能在返回的HTML本身中给出。表1.1显示了HTTP/1.0规范中定义的HTTP响应码[13]。
表1.1 HTTP/1.0响应码
聪明的读者可能会注意到,一些HTTP/1.0 RFC早期草稿中的响应码(如203、303、402)没有列出。在最终发布的RFC中,没有包含这些状态码。其中有一些状态码在HTTP/1.1中又回归了,但通常它们具有不同的描述和含义。IANA(Internet Assigned Numbers Authority,互联网数字分配机构)维护所有HTTP版本的状态码列表。但表1.1中所列状态码(最初在HTTP/1.0中定义)是最常用的。
很显然,某些状态码的含义有重叠。例如,你可能会疑惑,无法识别的请求,其状态码是400(错误请求)还是501(未实现)。把响应码设计为宽泛的形式,这样每个应用程序都可以选择最合适的状态码。该规范还指出,响应码是可扩展的,因此可以根据需要添加新代码而无须更改协议。这也是响应码会按首个数字分类的原因。一个新的响应码(如504)可能无法被已有的HTTP/1.0客户端所理解,但客户端知道请求是由于服务端的某种原因而失败,并且可以像处理其他5xx响应码一样来处理它。
HTTP响应首部
在返回响应的第一行之后,会有一到多行HTTP/1响应首部。请求首部和响应首部遵循同样的格式。在响应首部之后,是两个回车符,然后是响应体,如下加粗的部分所示:
随着HTTP/1.0的发布,HTTP语法得到了极大的扩展,能够使用它创建动态的功能丰富的应用程序,而不仅仅是HTTP/0.9时的简单文档获取程序。HTTP也变得越来越复杂,从大约700个单词的HTTP/0.9规范扩展到近20 000个单词的HTTP/1.0 RFC。即使发布了HTTP/1.0,HTTP工作组也认为其只是记录当前使用的权宜之计,并且已经着手HTTP/1.1的工作。我们之前提到过,HTTP/1.0的发布主要是为了给已经在使用的HTTP提供一些标准和文档,而不是为客户端和服务器定义新语法。除了新的响应码外,其他方法(例如PUT、DELETE、LINK和UNLINK)和当时正在使用的列在RFC的附录中的其他HTTP首部,在HTTP/1.1中变成了标准。HTTP的成功使得工作组在将HTTP推向全球的短短5年之后就难以跟上实际应用的步伐。
1.3.3 HTTP/1.1
如前所述,HTTP的第一个版本0.9版,为获取基于文本的文档提供了基本方法。现在其已被扩展到更完善的1.0版,并且在1.1版中被进一步标准化和完善。正如版本号所表示的,HTTP/1.1更像是对HTTP/1.0的调整,它没有从根本上改变协议。从0.9到1.0是一个较大的变化,增加了HTTP首部。HTTP/1.1做了进一步的改进,以便充分利用HTTP协议(例如,持久连接、强制响应首部、更好的缓存选项和分块编码)。更重要的是,它提供了一个正式标准,后来的万维网正是基于它构筑。虽然HTTP的基础知识很容易理解,但是里面许多错综复杂的细节、实现方式的不同,以及正式标准的缺乏使得它难以扩展。
HTTP/1.1的首个规范发布于1997年1月[14](HTTP/1.0规范发布之后仅仅过了9个月),于1999年6月[15]被新版本替换,然后于2014年6月第3个版本[16]发布。每个新版本的发布都会废除之前的版本。到如今,HTTP规范已经超过305页,有将近100 000个单词。这突显了它的成长速度。弄清楚HTTP复杂的使用方法至关重要。实际上,在撰写本书时,HTTP规范又更新了[17],此规范很快就会发布。从根本上说,HTTP/1.1和HTTP/1.0并没有太大的区别。但是在HTTP问世之后的20年中,网络的爆炸式增长,使其增加了许多新的功能,以及文档显示的新形式。
描述HTTP/1.1本身就需要一本书,但我们只讨论一些要点,为本书进行后面HTTP/2的讨论提供背景知识。HTTP/1.1的许多附加功能是通过HTTP首部引入的,HTTP首部本身是在HTTP/1.0中引入的,这意味着HTTP的基本结构在两个版本之间没有变化。强制首部和持久连接是HTTP/1.0语法的两个显著变化。
强制添加Host首部
HTTP请求行(如GET指令)中的URL不是一个包含绝对路径的URL(如http://www.example.com/section/page.html),而是一个只包含相对路径的URL(如/section/page.html)。在设计HTTP协议之初,一个Web服务器只能托管一个网站(但是这个网站上可以有很多部分和页面)。所以URL的主机名部分是不言自明的,因为在发送HTTP请求之前,用户肯定已经连接到了主机。如今,很多Web服务器上面有多个网站(虚拟主机托管),所以告诉服务器要访问哪个网站和访问哪个相对URL同样重要。此功能可以通过下面的方法实现:将HTTP请求中的URL修改为完整的包含绝对路径的URL。但如果采用这种方法,则很多现有的Web服务器和客户端都不能正常运行。所以,我们在请求首部中添加Host来实现该功能:
这个首部在HTTP/1.0中是可选的,但是在HTTP/1.1中它是必选项。下面的请求从技术上来讲格式并不正确,因为它声明自己使用HTTP/1.1,却没有提供一个Host首部:
根据HTTP/1.1的规范[18],请求应该被服务器拒绝(使用一个400状态码)。但是多数Web服务器很宽容,对于此类请求它们会使用一个默认的Host。
将Host作为必选项是HTTP/1.1的重要改进,这使得服务器能够充分利用虚拟主机托管技术,无须顾及因为新增网站而新加独立主机所带来的复杂性,从而使得网络繁荣发展。另外,如果没有这个改进,我们会更早遭遇IPv4的IP地址不足的限制。但从另一个角度来讲,如果没有这个方式来解决此限制,可能会更积极地推动IPv6的发展。但是在撰写本书时,IPv6还在推广的过程中,尽管它已经存在20多年了!
指定强制Host首部字段,而不是将相对URL更改为绝对URL,带来了一些争论[19]。HTTP/1.1引入的HTTP代理允许通过中间HTTP服务器连接到目标HTTP服务器。代理的语法要求所有的请求使用完整的绝对URL,但实际的Web服务器(也称为源服务器)要求强制使用Host首部。我们知道,使用Host首部对于避免破坏现有服务器是必要的,但它强制要求HTTP/1.1客户端和服务器使用虚拟主机式请求,只有这样才能完全兼容HTTP/1.1实现。有人认为,在HTTP的未来某个版本中,这种情况会得到更好的解决。HTTP/1.1规范声明,“为了支持在某些未来版本的HTTP中,转为使用绝对URL形式,服务器必须接受请求中的绝对URL形式,即使HTTP/1.1客户端只会在使用代理的请求中发送它们。”但是,稍后我们会看到,HTTP/2并未彻底解决此问题,而是使用:authority伪首部字段替换Host首部(请参阅第4章)。
持久连接(也就是KEEP-ALIVE)
HTTP中的另外一个被很多HTTP/1.0服务器所支持的重大更新是持久连接,但是它没被包含在HTTP/1.0的规范中。起初,HTTP仅仅是一个请求-响应协议。客户端打开连接,请求资源,获得响应,然后断开连接。随着互联网的媒体内容变得更加丰富,关闭连接被证明是一种浪费性能的行为。显示一个页面需要多个资源,所以关闭连接之后还要重新打开它,这导致了不必要的延迟。这个问题被一个新的Connection首部解决了,这个HTTP首部可以随着HTTP/1.0的请求被发送。通过指定此首部值为Keey-Alive,客户端可以要求服务器保持连接打开,以支持发送更多的请求:
服务器像往常一样响应,但如果它支持持久连接,它会在响应中包含一个Connection: Keep-Alive首部:
这个响应告诉客户端,在发送完响应之后,马上就可以在同一个连接上发送一个新的请求,所以服务器不用每次都关闭再重新打开连接。当使用持久连接时,想要知道响应何时完成可能会更困难。对于非持久连接,关闭连接是一个表明服务器已经完成了发送的好信号!所以,必须使用Content-Length首部来定义消息响应体的长度,以便当整个消息体传输完成后,客户端可以发送一个新请求。
客户端或服务器可以在任何时候关闭HTTP连接。关闭可能会意外发生(由于网络连接错误)或有意为之(例如,如果暂时不使用连接,那么服务器可以关闭连接以释放资源供其他连接使用)。因此,即使使用持久连接,客户端和服务器也应该监视连接并处理意外关闭的连接。一些请求会使情况变得更复杂。例如,你在电子商务网站上结账,你不会想要重复发起请求。
HTTP/1.1不仅将持久连接添加到文档标准中,还将其作为默认行为。即使响应中没有Connection:Keep-Alive首部,也可以假定任何HTTP/1.1连接都使用持久连接。如果服务器确实想要关闭连接,无论出于何种原因,则它必须在响应中显式包含Connection:close HTTP首部:
我们在本章前面的Telnet示例中谈到了这个问题。现在可以使用Telnet再次发送以下内容:
• 没有Connection:Keep-Alive首部的HTTP/1.0请求。你应该可以看到,在发送响应后服务器自动关闭连接。
• 相同的HTTP/1.0请求,但具有Connection:Keep-Alive首部。你应该可以看到,连接保持打开状态。
• 一个HTTP/1.1请求,带或不带Connection:Keep-Alive首部。你应该可以看到,在默认情况下连接保持打开状态。
对于HTTP/1.1客户端,包含Connection:Keep-Alive首部的请求并不罕见,而且它还是默认值。同样,服务器有时会在HTTP/1.1响应中包含此首部,尽管这是不必要的。
在此基础上,HTTP/1.1增加了管道的概念,因此应该可以通过同一个持久连接发送多个请求并按顺序获取响应。例如,如果Web浏览器正在处理HTML文档,并且发现需要CSS文件和JavaScript文件,它应该能够将这些文件的请求一起发送,并按顺序获取响应,而不需要等待第一个请求响应完成后才发出第二个请求。下面是一个例子:
由于某些原因(见第2章),管道化并没有流行起来,并且客户端(浏览器)和服务器对管道化的支持都很差。因此,虽然持久连接允许在同一个TCP上顺序发出多个请求,这也是一个很好的性能改进,但大多数HTTP/1.1的实现仍然是遵循请求响应再请求再响应的模式的。当一个请求被处理时,HTTP连接被阻塞,不能用于其他请求。
其他新功能
HTTP/1.1引入了很多其他的新功能,包含:
• 除了在HTTP/1.0中定义的GET、POST和HEAD方法之外,HTTP/1.1又定义了新的方法,如PUT、OPTIONS和比较少见的CONNECT、TRACE及DELETE。
• 更好的缓存方法。这些方法允许服务器指示客户端将资源(例如CSS文件)存储在浏览器的缓存中,以便在以后需要时重复使用。在HTTP/1.1中引入的Cache-Control HTTP首部比HTTP/1.0中的Expires首部的选项更多。
- HTTP cookies,允许HTTP维护状态。
- 引入字符集(如本章的一些例子所示),在HTTP响应中新增语言选项。
- 支持代理。
- 支持权限验证。
- 新的状态码。
- 尾随首部(在4.3.3节中讨论)。
HTTP协议不断添加新的首部以进一步扩展功能,其中许多是出于性能或安全原因。HTTP/1.1规范并未将HTTP/1.1固化,其鼓励定义新的首部,甚至还使用一个章节专门讨论如何定义和记录首部[20]。正如之前提到的,其中的一些首部是出于安全原因添加的,网站用它们来告诉Web浏览器打开某些可选的安全保护,这样服务端就不必额外实现安全相关的功能(除了发送这些响应首部的功能)。曾经有一个惯例,是在这些首部中包含一个X-来表明它们没有被正式标准化(X-Content-Type、X-Frame-Options或X-XSS-Protection),但这个约定已经不推荐使用了[21],这就导致新的实验性首部很难与HTTP/1.1规范中的首部区分开来。通常,这些首部都有自己的RFC标准文档(Content-Security-Policy[22]、Strict-Transport- Security[23]等)。