在聊 HTTP 之前,我们有必要简单的介绍一下 Internet 和 Web。
Internet 的本质是将世界各地的电脑连接起来,基于 IP 协议组成一个网络。然后在此基础上,提供各种各样的应用,所谓应用就是软件,这些软件往往是分布式的,也就是部署和运行在多台电脑上,只有这样,应用才能提供一个完整的服务,这些服务的本质是信息的存储、交换和共享,比如 World Wide Web (万维网,或简称为 Web),DNS,email,文件共享服务,互联网电话等等。但是随着时代的发展,渐渐地 Web 发展越来越壮大,其他的应用和服务慢慢退出了历史舞台。因为我们接触最多的就是 Web,并且现在 email 也主要使用 web-mail,所以我们常常把互联网等同于 Web。Web 这个应用最初只是用来为科研结构(比如大名鼎鼎的 CERN)提供文档共享、引用的服务,远不如我们现在所熟知的 Web 这般强大。
完整的 Web 服务包括 3 个方面的技术:
- HTTP:定义了文档如何在客户端和服务器之间传输。
- URI:定义了文档的唯一标识符,也就是在哪里可以找到文档。
- HTML:定义了如何编写文档。(其中还包含了 CSS,JavaScript)。
我们这里重点要说的就是 HTTP。HTTP 是 Hypertext Transfer Protocol 的缩写,也就是所谓的超文本传输协议。那什么是超文本?我们来看牛津词典上对 Hypertext 的解释:text stored in a computer system that contains links that allow the user to move from one piece of text or document to another。简单的说,超文本就是一种含有链接的文本,点击链接可以跳转到其他文本。超文本把本来一篇篇孤立的文本,通过链接的方式连接起来,使它们可以互相查阅、引用。毕竟 Web 发明的初衷就是为了科学研究用途,搞科研需要查阅大量的文献,通过超文本,就使得文献的查阅和引用变得方便和高效了很多。
HTTP 发明的最初,只是用来传输文档的,但是马上开发者就意识到,它还可以用来传输其他资源,比如图片、JSON 数据等,所以 HTTP 这个名词跟它实际行使的功能有了一定的出入。在互联网领域,这是一个非常普遍的现象,因为互联网诞生的晚、发展的快,很多时候人们定义了一个新技术、新术语,但是很快这个新技术、新术语的实际内涵就会变得膨胀起来,甚至是背离了它被发明的初衷,这样的例子比比皆是。所以我们有时候不必纠结一个名词,只要知道它最初是怎么来的,了解它的历史背景,清楚它的内涵和外延就可以了。更重要的,我们要关注它现在是什么样,未来又会变成什么样。
HTTP 之所以能够迅速流行,并大获成功,得益于它设计得足够简单(特别是它的第一个正式版本 0.9)。人们都喜欢简单的东西,简单的协议,意味着容易实现,从而客户端和服务器可以迅速的支持 HTTP。
HTTP/0.9 有多简单呢?这行请求就是它的全部:GET /。是的,没有请求头字段,没有 Cookie,什么都没有。只有一个 GET 方法和一个 path。而且它只支持传输文档,其他类型的资源文件,一概不支持。这里有人就要问了,HTTP/0.9 怎么没有指定一个 host,那它怎么知道要找哪台服务器要文档呢?这是因为 HTTP/0.9 不关心 host 是谁,它假定你已经连接到了那台 host 的 80 端口,所以在发送 HTTP/0.9 消息时,要先确保连上了那台目标 host。
很多时候,一个事物的优点和缺点是同源的。设计简单的 HTTP/0.9 慢慢跟不上 Web 发展的速度。因为 Web 越来越复杂,传输的文档越来越多,HTTP/0.9 过于简单的设计导致它无法满足越来越复杂的需求,渐渐被开发者所抛弃,几乎所有的 Web 服务器都实现了自己在 HTTP/0.9 上扩展的功能。这也是 Web 的一大特点:规范总是落后于实现,实现总是走在规范的前面。规范更像是对现实中的实现的一个总结,而不是一个预先的约束——毕竟谁也无法预知 Web 会如何发展,更不知道它会发展得这么快。
于是,HTTP/1.0 马上就来了。相比 HTTP/0.9,它增加了如下功能:
- 更多的请求方法:HEAD 和 POST
- 请求/响应行增加了关于 HTTP 版本的说明,不写默认就是 HTTP/0.9
- 增加了请求/响应头部字段
- 响应增加了一个三位数的代码,用来说明响应是否成功
准确来说,HTTP/1.0 并非增加了新的语法或功能,让客户端和服务器去实现;而是对现实的 Web 客户端和服务器所自然演进出来的新功能的一个记录。所以 HTTP/1.0 事实上并非一个正式的标准,而只是一个备忘。
这里着重说一下 POST 方法,POST 方法让 HTTP 请求像响应一样,拥有了 body,可以像服务器传输一些内容。
而且,正是由于增加了请求/响应头部字段,才让 HTTP 拥有了传输文档以外的媒体资源的能力,比如图片、音频、视频、脚本等。通过在响应头中指明响应体的 Content-type,客户端就知道服务器响应的是什么类型的资源了。
很快,HTTP/1.1 就来了,它有两个最大的变动:
- 请求头必须带上 Host 字段:在 HTTP/1.1 之前,请求头是可以不带 Host 字段的,因为客户端在发起 HTTP 请求之前,必须保证已经跟服务器建立了 TCP 连接,而且以前的服务器只对应一个网站。既然已经跟服务器连接上了,就不需要多此一举再说明 Host 是谁了。但是随着虚拟主机技术的发展,同一台服务器,可以作为多个网站的主机,所以如果不指明 Host 是谁的话,就不知道请求到底要发给谁。
- 持久化连接:在 HTTP/1.1 之前,发一个请求,会建立一条 TCP 连接,响应结束后,连接会关闭,再等待下一个请求,重复此过程。之所以设计成这样是因为早期的请求只是要一份文档,请求的资源数量很少。但是随着网页的复杂度提升,一个网页要完整的渲染完毕,往往需要几十甚至上百份静态资源,这样 TCP 连接的重复建立和断开会消耗很多的时间和资源。所以在 HTTP/1.1 引入了持久化连接,在请求头中将 Connection 字段默认设置成 keep-alive,TCP 连接在建立后,是不会马上关闭的,后续请求同一个主机,会复用此连接,这样就节省了大量的时间和资源。
但是,即便是引入了持久化连接,HTTP/1.1 仍然有着严重的性能问题,因为 HTTP 的请求/响应只能串行进行,一条 TCP 连接只能跑一对请求/响应,下一个请求必须要等到上一个请求的响应回来后,才能发出去,这就导致:如果前一个请求因为各种原因延迟了、卡住了,那么后面所有的请求都发不出去。这就是所谓的 head-of-line blocking(队头阻塞)问题。
那如何解决这个问题了。有这么几个思路:
- pipeline 技术:同时发多个请求出去。遗憾的是,此项技术因为各种原因没有被广泛支持,基本等于不可用。
- 使用多条 TCP 连接来发请求:通常浏览器对每个域名最多只建立 6 条连接,我们可以通过子域名、cdn 的方式,将图片、样式、脚本等静态资源文件部署在不同的子域名上,这样可以变相的增加了浏览器队服务器的最大连接数。由于每条连接都是互相独立的,所以不会有 HOL blocking 的问题。但是这种方案也是有缺点的,建立 TCP 连接需要时间(3 次握手:SYN, SYN-ACK, ACK),维护 TCP 连接需要内存和算力,TCP 连接的数量越多,对客户端和服务器的压力就越大,并且 TCP 连接还有 slow start 和占用带宽的问题。
- 减少请求数量,每个请求多处理一些数据:包括缓存一些静态资源,合并资源(比如雪碧图,CSS 合并,JavaScript 打包),将部分资源内联在其他文件中(比如图片可以使用 svg,或者使用 base64 编码)等。
HTTP/2 以下的版本所传输的内容格式都是基于文本的,所谓基于文本,也就是说它传输的内容是一个 ASCII 字节流,客户端/服务器收到后,要先将其解码为文本才能知道具体内容的含义。HTTP/2 传输的内容格式是二进制,客户端/服务器无需对内容解码,而是直接对字节进行分割,就能知道每个字节或字节组代表的含义是什么(字节流被划分成了不同的区间,每个区间的语义是事先定义好了的)。