17 KiB
漫游 HTTP/2
自从我写了上一篇博文之后,就再也找不到空闲时间写文章了。今天我终于可以抽出时间写一些关于 HTTP 的东西。
我认为每一个 web 开发者都应该对 HTTP 协议有所了解,这样才能帮助你更好的完成开发任务。
在这篇文章中,我将讨论 HTTP 什么是,怎么产生的,它的地位,以及我们应该怎么使用它(这里不太确定)。
HTTP 是什么
首先我们要明白 HTTP 是什么。HTTP 是一个基于 TCP/IP
的应用层通信协议,它定义了客户端和服务端在互联网传输中所必要的信息,并且制定成标准。HTTP 是在应用层中抽象出的一个标准,使得主机(客户端和服务端)之间的通信得以通过 TCP/IP
来产生请求和响应。TCP 默认使用的端口是 80
,当然也可以使用其他端口,比如 HTTPS 使用的就是 443
端口。
HTTP/0.9
- 单行协议 (1991)
HTTP 最早的文档可以追溯到 1991 年,那时候的版本是 HTTP/0.9
,该版本极其简单,只有一个的动作的 GET
。如果客户端要访问服务端上的一个页面,只需要如下非常简单的请求:
GET /index.html
服务端对应的返回如下:
(response body)
(connection closed)
就这么简单,服务端捕获到请求后立马返回 HTML 并且关闭连接,在这之中
- 没有头信息(headers)
- 仅支持
GET
这一种请求方法 - 必需返回 HTML
如同你所看到的,当时的 HTTP 协议只是一块基础的垫脚石。
HTTP/1.0 - 1996
在 1996 年,新版本的 HTTP 对比之前的版本有了极大的改进,同时也被命名为 HTTP/1.0。
与 HTTP/0.9
只能返回 HTML 不同的是,HTTP/1.0
支持处理多种返回的格式,比如图片、视频、文本或者其他格式的文件。它还增加了更多的请求方法(如 POST
和 HEAD
),请求和相应的格式也相应做了改变,两者都增加了头信息;引入了状态码来定义返回的特征;支持多种文件格式,支持用户验证信息、缓存、多种编码格式等等
一个简单的 HTTP/1.0 请求大概是这样的:
GET / HTTP/1.0
Host: kamranahmed.info
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*
正如你所看到的,在请求中附带了客户端中的一些个人信息、响应类型要求等内容。这些是在 HTTP/0.9
无法实现的,因为那时候没有头信息。
一个请求的例子如下所示:
HTTP/1.0 200 OK
Content-Type: text/plain
Content-Length: 137582
Expires: Thu, 05 Dec 1997 16:00:00 GMT
Last-Modified: Wed, 5 August 1996 15:55:28 GMT
Server: Apache 0.84
(response body)
(connection closed)
从 HTTP/1.0
早期开始,在状态码 200
之后就附带一个短语(你可以用来描述状态码)。
在这个早期的版本中,请求和响应的投信息必需为 ASCII
编码,但是响应的内容可以是任意类型,如图片、视频、HTML、文本或其他类型,服务器可以返回任意内容给客户端。所以,在 HTTP
中的“超文本(Hyper Text)”成了名不副实。 HMTP
或超媒体传输协议(Hypermedia transfer protocol)可能会更有意义,但是我猜我们还是会一直沿用这个名字。
一个 HTTP/1.0
主要的缺点就是比不能在一个连接内拥有多个请求。这意味着,当客户端需要从服务器获取东西时,必需建立一个新的 TCP 连接,并且处理完单个请求后连接即被关闭。需要下一个东西时,你必需重新建立一个新的连接。这样的坏处在哪呢,假设你要访问一个有 10
张图片,5
个样式表(stylesheet)和 5
个 JavaScript 总计 20
个文件才能完整展示的一个页面。由于一个连接在处理完成一次请求后即被关闭,所以将有 20
个单独的连接,每一个文件都将通过各自对应的连接单独处理。当连接数量变得庞大的时候就会面临严重的性能问题,因为 TCP
启动需要经过三次握手,才能缓慢开始。
三次握手
三次握手是一个简单的模型,所有的 TCP
连接的建立需要在三次握手中传输一系列数据包。
SYN
- 客户端选取一个随机数,我们称为x
,然后发送给服务器SYN ACK
- 服务器响应对应请求的ACK
包中,包含了一个由服务器随机产生的数字,我们称为y
,并且把客户端发送的x+1
,一并返回给客户端。ACK
- 客户端在从服务器接受到y
之后把y
加上1
然后带上x+1
作为一个ACK
包返回给服务器。
一旦三次握手完成后,客户端和服务器之间就可以开始数据共享。值得注意的是,当客户端发出最后一个 ACK
数据包后,就可以立刻向服务器发送应用数据包,而服务器则需要等到收到这个 ACK
数据包后才能接受应用数据包。
然而,某些 HTTP/1.0 的实现试图通过新引入一个称为 Connection: keep-alive
的 header 克服这一问题,这个 header 意味着告诉服务器“嘿,服务器,请不要关闭此连接,我还要用它”。但是,这并没有得到广泛的支持,问题依然存在。
除了无连接,HTTP 还是一个无状态的协议,即服务器不维护有关客户端的信息。因此每个请求要从之前旧的无关的请求中获取数据来满足服务器所要求的信息。所以,这增加了推波助澜的作用,客户端除了需要新建大量连接之外,在每次连接中还需要发送许多无关的数据。
HTTP/1.1
- 1999
HTTP/1.0
经过仅仅 3 年,下一个版本,即 HTTP/1.1
在 1999 年发布,改进了它的前身很多问题,主要的改进包括:
-
增加了许多 HTTP 请求方法,包括
PUT
,PATCH
,HEAD
,OPTIONS
,DELETE
-
主机标识符在
HTTP/1.0
并不是必需的,而在HTTP/1.1
是必需的 -
如上所述的持久连接。在
HTTP/1.0
中每个连接只有一个请求并在该请求结束后被立即关闭,这导致了性能问题和增加了延迟。 HTTP/1.1 引入了持久连接,即连接在默认情况下是不关闭并保持开放的,这允许多个连续的请求使用这个连接。要关闭连接只需要在头信息加入Connection: close
,客户通常发送最后一个请求时的头信息中加入这个就能安全地关闭连接。 -
新版本还引入了“管线化(pipelining)”的支持,客户端可以在同一个连接内不用服务器返回响应,发送队列内多个请求给服务器。但是你可能会问了,客户端如何知道哪个是第一个响应的内容,下一个响应的内容又应该是哪个呢,解决这个问题,头信息必须有
Content-Length
,客户可以使用它来确定哪些响应结束之后可以开始等待下一个响应。
值得注意的是,为了从持久连接或管线化中受益, 头部信息必需包含
Content-Length
,因为这会客户端知道什么时候完成了传输,然后它可以发送下一个请求(在正常的按依次顺序发送请求)或开始等待下一个响应(启用管线化时)。
但是,使用这种方法仍然有一个问题。那就是,如果数据是动态的,服务器无法提前知道内容。那么在这种情况下,你就不能使用这种方法中获益了吗?为了解决这个问题,HTTP/1.1 引进了分块编码。在这种情况下,服务器可能会忽略内容长度来支持分块编码。但是,如果它们都不可用,那么连接必须在请求结束时关闭。
-
在动态内容的情况下分块传输,当服务器在传输开始时,无法得到内容长度时,它可能会开始发送(分成多个小块)部分的内容,并在传输时为每一个小块添加
Content-Length
。并且当发送完所有的数据块后,即整个传输已经完成后,它发送一个空的小块,以便确定客户端的传输已完成。为了通知客户块传输的信息,服务器在头信息中包含了Transfer-Encoding: chunked
-
不像 HTTP/1.0 中有基本身份验证,HTTP/1.1 包括摘要认证(digest authentication)和代理验证(proxy authentication)
-
缓存
-
范围请求(Byte Ranges)
-
字符集
-
内容协商(Content Negotiation)
-
客户端 cookies
-
支持压缩
-
新的状态码
-
等等
我不打算在这里讨论所有 HTTP/1.1
的特性,因为你可以围绕这个话题找到很多关于这些的讨论。我建议你阅读 RFC 的文档来获取 HTTP/1.0
和 HTTP/1.1
版本之间的主要差异。
HTTP/1.1
在 1999 年推出,到现在已经是多年前的标准。虽然,它改善了很多之前的问题,但是网络日新月异,它开始越来越不能满足现在的需求。相比之前,加载网页更是一个资源密集型任务,打开一个简单的网页已经需要建立超过 30 个连接。HTTP/1.1
具有持久连接,为什么有这么多连接呢?其原因是,在任何时刻 HTTP/1.1
只能有一个未完成的连接。 HTTP/1.1
试图通过引入管线来解决这个问题,但它并没有完全地解决。因为一旦管线遇到了缓慢的请求或庞大的请求,后面的请求便被阻塞住,它们必需等待上一个请求完成。为了克服 HTTP/1.1
的这些缺点,开发人员开始采用的解决方法,例如使用 spritesheets,在 CSS 中绘制图像,单个 CSS / JavaScript 文件,动态渲染等。
SPDY - 2009
谷歌走在业界前端,为了使网络速度更快,提高了网络安全,同时减少网页的等待时间,他们开始实验替代的协议。在 2009 年,他们宣布了 SPDY
。
SPDY
是谷歌的商标,而不是一个缩写。
显而易见的是,如果我们继续增加带宽,提升网络性能,但是实际上带来的体验提升极其有限。但是如果把这些优化放在等待时间上,比如减少等待时间,将会有持续的性能提升。这就是 SPDY
对于之前协议优化的核心思想,减少等待时间来提升网络性能。
对于那些不知道其中区别的人,等待时间就是延迟,即数据从源需要多长时间的到达目的地(单位为毫秒)和数据传输的宽带(比特每秒)。
SPDY
的特点包括,复用,压缩,优先级,安全性等。我不打算展开 SPDY
的细节。在下一章节,当我们将介绍 HTTP/2,这些都会被提到,因为 HTTP/2
大多特性是从 SPDY 受启发的。
SPDY
没有试图取代 HTTP,它所在的应用层是 HTTP 的传输层更上一层,它只是在请求被发送之前做了一些修改。它开始成为真正意义上的标准前,大多数浏览器都开始支持了。
2015年,在谷歌不想有两个相互竞争的标准,所以他们决定将其合并到 HTTP 协议同时产生了 HTTP/2
。
HTTP/2
- 2015
现在你必须相信,我们为什么需要 HTTP 协议的另一个版本。 HTTP/2
是专为了低延迟地传输内容设计。主要特点和与 HTTP/1.1
的差异包括
- 使用二进制替代文本
- 多路传输 - 多个异步 HTTP 请求可以使用单一连接
- 报头使用 HPACK 压缩
- 服务器推送 - 单个请求多个响应
- 请求优先级
- 安全性
1. 二进制协议
HTTP/2
通过使其成为一个二进制协议以解决 HTTP/1.x 中存在的延迟问题。作为一个二进制协议将更容易地被解析但可读性却不如 HTTP/1.x
。帧(frames)和流(stream)的概念组成了 HTTP/2
的主要部分。
帧和流
现在 HTTP 消息是由一个或多个帧组成。HEADERS
帧承载了元数据(meta data)和 DATA
帧。同样的还有其他类型的帧(HEADERS
, DATA
, RST_STREAM
, SETTINGS
, PRIORITY
等等),这些你可以通过HTTP/2 的文档来查看。
每个 HTTP/2
请求和响应都分成帧并且被赋予一个唯一的流 ID。帧就是一小片分隔后的二进制数据。帧的集合称为流,每个帧都标识了其所属的流和流的 ID,所以在同以个流下的每个帧具有共同的报头。值得注意的是,由客户端发起的请求流使用了奇数作为 ID,从服务器响应的流使用了偶数作为 ID。
除了 HEADERS
帧和 DATA
帧,另一个值得一提的帧是 RST_STREAM
。这是一个特殊的帧类型,用来中止流即客户可以发送此帧让服务器知道,我不再需要这个流了。在 HTTP/1.1
中做的唯一方法服务器停止响应客户端发送的请求,这样造成了延迟,因为之后要发送请求时,就要必须打开一个新的请求。而在 HTTP/2 ,客户端可以使用 RST_STREAM
来停止接收特定的数据流,而连接仍然可以被其他请求使用。
2. 多路传输
如同上面所说,HTTP/2
是一个使用帧和流来传输请求与相应的二进制协议,一旦建立了 TCP 连接,相同连接内的所有流都可以同个这个 TCP 连接异步发送。反过来说,服务器也可以使用同样异步的方式返回相应,也就是说这些响应可以是无序的,客户端使用分配给流的 ID 来识别数据包所属的流,这也解决了 HTTP/1.x 中请求管道被阻塞的问题,即客户端不必花时间等待其他请求被处理即可发送其他请求。
3. HPACK 请求头部压缩
RFC 花了一篇文档的篇幅来介绍针对优化发送信息的头部,它的本质是当我们在同一客户端上不断地访问服务器时,许多冗余数据在头部中被反复发送,有时候仅仅是 cookies 就能增加头信息的大小,这会占用许多宽带和增加传输延迟。为了解决这个问题,HTTP/2 引入了头信息压缩。
不像请求和响应那样,头信息中的信息不会被以 gzip
或者其他格式压缩。客户端和服务器同时维护一张头信息表,储存了使用了哈夫曼编码进行编码后的头信息的值,并且后续请求中若出现同样的字段则忽略重复值(例如用户代理(agent)等),只发送存在两边信息表中它的引用即可。
我们说的头信息,是指在 HTTP/1.1 的基础上增加了一些伪头信息,如 :scheme
,:host
和 :path
。
4. 服务器推送
服务器推送是 HTTP/2
的另一个巨大的特点。对于服务器来说,当它知道客户端需要一定的资源后,它可以把数据推送到客户端甚至没有客户端要求它。例如,假设一个浏览器在加载一个网页时,它解析了整个页面,发现有一些内容必需要从服务端获取,然后发送相应的请求到服务器以获取这些内容。
服务器推送减少了传输这些数据需要来回请求的次数。它是如何做到的呢?服务器通过发送一个名字为 PUSH_PROMISE
特殊的帧通知到客户端“嘿,我准备要发送这个资源给你了,不要再问我要了。”这个 PUSH_PROMISE
帧就把要产生推送的流和流的 ID 联系在一起,也就是说这个流将会被服务器推送到客户端上。
5. 请求优先级
当流被打开的时候,客户端可以把 HEADERS
帧中优先级分配到流中。在任何时候,客户端都可以发送 PRIORITY
帧来改变数据流的优先级。
如果没有任何优先级信息,服务器将异步无序地处理这些请求。如果数据流分配了优先级,服务器将在这个优先级的基础上来分配多少资源来处理这个请求。
6. 安全性
在是否强制使用 TLS
来增加安全性的问题上产生了大范围的讨论,讨论的结果是不强制使用。然而大多数厂商只有在使用 TLS 时才能使用 HTTP/2
。所以 HTTP/2
虽然不需要规范来强制加密,但是加密已经约定俗成。这样在实现 HTTP/2
时,都会依赖强制使用 TLS
来加密。依赖的 TLS
的最低版本为 1.2
,同时需要布署 ephemeral 密钥。
到现在 HTTP/2
已经完全超越了 SPDY 并且还在不断成长,HTTP/2 有很多关系性能的提升,我们应该开始布署它。
如果你想更深入的了解细节,请访问 link to specs 和 link demonstrating the performance benefits of HTTP/2。请在留言板写下你的疑问或者评论,最后如果你发现有那些错误,请同样留言指出。
我们之后再见~
via: http://kamranahmed.info/blog/2016/08/13/http-in-depth/?utm_source=webopsweekly&utm_medium=email
作者:Kamran Ahmed 译者:译者ID 校对:校对者ID