前言:✍️ 你们好啊,还是老样子,翻译一篇~
- 原文链接
Begin
HTTP 是每个网页开发者都应该了解的协议,因为他是整个互联网的基础。了解 HTTP 肯定可以帮助你开发更好的应用程序。
本文, 我们将探讨 HTTP 是什么,它是如何产生的,它的现状及我们是如何到达现在的程度的。
什么是 HTTP?
首先,HTTP 是什么?HTTP 是一种基于 TCP/IP 的应用层通信协议,它规范了客户端和服务器之间的通信方式。它定义了在互联网上请求和传输内容的方式。通过应用层协议,我指的是它仅仅是一个抽象层,用于规范主机(客户端和服务器)之间的通信。HTTP 本身依赖于 TCP/IP 来在客户端和服务器之间传递请求和响应。默认情况下,使用 TCP 端口 80,但也可以使用其他端口。然而,HTTPS 使用端口 443。
HTTP/0.9 - 一句话概述 (1991)
HTTP 的第一个记录版本是 HTTP/0.9,于 1991 年提出。它是有史以来最简单的协议,只有一个名为 GET 的方法。如果客户端需要访问服务器上的某个网页,它会发送以下简单的请求:
GET /index.html
服务器的响应将如下所示:
(response body)
(connection closed)
也就是说,服务器会接收到请求,以 HTML 形式回复,并在内容传输完成后立即关闭连接。在 HTTP/0.9 中,没有持久连接的概念,每个请求都会建立一个新的连接。
- No headers
GET
was the only allowed method- Response had to be HTML
正如你所看到的,这个协议实际上只是为未来的发展铺平了道路,没有更多的功能。
HTTP/1.0 - 1996
在 1996 年,HTTP 的下一个版本,即 HTTP/1.0 得到了显著改进,相比于初始版本有了很大的提升。
与仅设计用于 HTML 响应的 HTTP/0.9 不同,HTTP/1.0 现在可以处理其他响应格式,如图像、视频文件、纯文本或任何其他内容类型。它增加了更多的方法(例如 POST 和 HEAD),请求/响应格式发生了变化,HTTP 头被添加到请求和响应中,状态码被添加以标识响应,引入了字符集支持,多部分类型、授权、缓存、内容编码等都被包括在内。
以下是一个示例 HTTP/1.0 请求和响应的样式:
GET / HTTP/1.0
Host: cs.fyi
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_10_5)
Accept: */*
正如您所看到的,在请求中,客户端还发送了个人信息、所需的响应类型等。而在 HTTP/0.9 中,客户端无法发送此类信息,因为那时还没有头部(header)的概念。
对于上述请求的示例响应可能如下所示:
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(HTTP 后跟版本号),然后是状态码 200,后面是状态码的原因短语(或者可以说是状态码的描述)。
在这个更新的版本中,请求和响应头仍然保持为 ASCII 编码,但是响应体可以是任何类型,例如图像、视频、HTML、纯文本或任何其他内容类型。因此,现在服务器可以向客户端发送任何内容类型;在引入这个功能后不久,HTTP 中的"Hyper Text"一词就变得不准确了。"HMTP"或"Hypermedia Transfer Protocol"可能更合适,但我猜我们将一直沿用这个名字。
HTTP/1.0 的一个主要缺点是无法在一个连接中进行多个请求。也就是说,每当客户端需要从服务器获取某些内容时,它都必须打开一个新的 TCP 连接,等到单个请求完成后,连接就会关闭。而对于任何下一个需求,都必须使用新的连接。为什么这样不好呢?假设你访问一个包含 10 个图像、5 个样式表和 5 个 JavaScript 文件的网页,总共有 20 个需要在请求网页时获取的项目。由于服务器在请求完成后立即关闭连接,将会有一系列的 20 个独立连接,每个项目都在其各自的连接上逐个提供。这么多连接会导致严重的性能问题,因为需要新建立 TCP 连接会带来显著的性能损失,包括三次握手和慢启动等过程。
三次握手
三路握手的简单形式是,所有 TCP 连接都以三路握手开始,其中客户端和服务器在开始共享应用程序数据之前共享一系列数据包。
SYN
- 客户端选择一个随机数,例如 x,并将其发送给服务器SYN ACK
- 服务器通过向客户端发送一个 ACK(确认)数据包来确认请求。该 ACK 数据包由服务器选择的一个随机数 y 和客户端发送的数字 x+1 组成,其中 x 是客户端发送的数字。ACK
- 客户端将从服务器收到的数字 y 加一,并将该数字 y+1 作为 ACK(确认)数据包发送回去。
完成三次握手后,客户端和服务器之间可以开始进行数据共享。需要注意的是,客户端在发送最后一个 ACK 数据包后可以立即开始发送应用程序数据,但服务器仍然需要等待接收到 ACK 数据包才能满足请求。
然而,一些 HTTP/1.0 的实现尝试通过引入一个名为"Connection: keep-alive"的新头部来克服这个问题,该头部的作用是告诉服务器:"嘿,服务器,请不要关闭这个连接,我还需要它"。但是,这个特性并没有得到广泛支持,问题仍然存在。
除了无连接性之外,HTTP 还是一种无状态协议,也就是说服务器不会维护关于客户端的信息,因此每个请求都必须包含服务器完成请求所需的信息,而不与任何旧请求相关联。因此,这进一步加剧了问题,即除了客户端需要打开大量连接之外,还需要在网络上发送一些冗余数据,导致带宽使用增加。
HTTP/1.1 - 1997
在仅仅三年后的 1999 年,HTTP/1.0 的下一个版本 HTTP/1.1 发布了,它对前一版本进行了许多改进。HTTP/1.1 相对于 HTTP/1.0 的主要改进包括:
- 新增了新的 HTTP 方法,引入了
PUT、PATCH、OPTIONS
和DELETE
方法。 - 在 HTTP/1.0 中,主机头(Host header)不是必需的,但在 HTTP/1.1 中,它是必需的。
- 正如上面讨论的那样,在 HTTP/1.0 中,每个连接只有一个请求,连接在请求完成后立即关闭,这导致性能下降和延迟问题。HTTP/1.1 引入了
持久连接(persistent connections)
,即连接默认不关闭,保持打开状态,允许连续发送多个请求。要关闭连接,请求中必须包含"Connection: close"
头部。通常,客户端会在最后一个请求中发送这个头部,以安全地关闭连接。 - 它还引入了对
流水线处理(pipelining)
的支持。在流水线处理中,客户端可以在同一个连接上发送多个请求,而无需等待服务器的响应,而服务器必须按照请求接收的顺序发送响应。但是,你可能会问,客户端如何知道第一个响应下载完成的时间点以及下一个响应的内容开始的时间点!好的,为了解决这个问题,必须存在Content-Length
头部,客户端可以利用它来确定响应的结束位置,然后开始等待下一个响应。
值得注意的是,为了从持久连接或流水线处理中获益,响应中必须包含
Content-Length
头部。这样可以让客户端知道传输何时完成,从而可以发送下一个请求(按照正常的顺序发送请求),或者开始等待下一个响应(当启用流水线处理时)。
但是,这种方法仍然存在一个问题。那就是,如果数据是动态的,服务器无法事先确定内容长度怎么办?在这种情况下,你真的无法从持久连接中获益,对吗?为了解决这个问题,HTTP/1.1 引入了
分块编码(chunked encoding)
。在这种情况下,服务器可以省略Content-Length
头部,而是将响应分块发送(稍后会详细介绍)。然而,如果这两种方法都不可用,那么连接必须在请求结束时关闭。
分块传输(Chunked Transfers)
:在动态内容的情况下,当服务器无法在传输开始时确定Content-Length
时,它可以开始将内容分成多个片段(逐个片段)并在发送时为每个片段添加 Content-Length。当所有的片段都发送完毕,也就是整个传输完成时,服务器会发送一个空的片段
,即 Content-Length设置为零的片段
,以通知客户端传输已完成。为了向客户端通知分块传输,服务器会在头部包含Transfer-Encoding: chunked
。- 与只支持
基本身份验证
的 HTTP/1.0 不同,HTTP/1.1 引入了摘要身份验证(digest)
和代理身份验证(proxy authentication)
。 - 缓存(Caching)
- 字节范围(Byte Ranges)
- 字符集(Character sets)
- 语言协商(Language negotiation)
- 客户端 Cookie(Client cookies)
- 增强的压缩支持
- 新的状态码
- ......
在这篇文章中,我不打算详细讨论所有的 HTTP/1.1 功能,因为这本身就是一个专题,你已经可以找到很多相关资料了。我建议你阅读的一个文件是《HTTP/1.0 与 HTTP/1.1 的主要区别》,以下是原始 RFC 的链接,供那些追求完美的人使用。
HTTP/1.1 于 1999 年推出,并成为多年的标准。尽管它相对于前身有很多改进,但随着网络每天都在变化,它开始显现出过时的迹象。如今,加载网页比以往任何时候都更加资源密集。现如今,一个简单的网页需要打开超过 30 个连接。你可能会问,既然 HTTP/1.1 具有持久连接,为什么还需要这么多连接?原因是在 HTTP/1.1 中,任何时刻只能有一个未完成的连接。HTTP/1.1 尝试通过引入流水线处理来解决这个问题,但由于头部阻塞的存在,慢速或繁重的请求可能会阻塞后面的请求,一旦一个请求被卡在流水线中,它将不得不等待下一个请求被满足。为了克服 HTTP/1.1 的这些缺点,开发人员开始实施一些解决方法,例如使用精灵图、在 CSS 中编码图像、单个庞大的 CSS/JavaScript 文件、域分片等。
SPDY - 2009
谷歌开始尝试使用替代协议来加速网络并改善网页安全性,同时减少网页的延迟。在 2009 年,他们宣布了 SPDY 协议。
SPDY 是谷歌的商标,不是一个首字母缩写。
研究发现,如果我们不断增加带宽,网络性能一开始会提高,但到了某个点之后,性能的提升就不那么明显了。但是,如果我们降低延迟,即不断减少延迟,就能持续获得性能提升。这就是 SPDY 协议背后的核心思想,通过减少延迟来提高网络性能。
对于不了解这两者区别的人来说,延迟是指数据在源和目的地之间传输所需的时间延迟(以毫秒为单位),而带宽是指每秒传输的数据量(以每秒位数为单位)。
SPDY 的特性包括多路复用、压缩、优先级设置、安全性等等。我不会详细介绍 SPDY 的细节,因为当我们在下一节深入了解 HTTP/2 时,你会对此有所了解,正如我之前所说,HTTP/2 在很大程度上受到了 SPDY 的启发。
确实,SPDY 并没有真正尝试取代 HTTP,它是在应用层上存在的 HTTP 的一个翻译层,在将请求发送到网络之前修改了请求。它开始成为事实上的标准,并且大多数浏览器开始实现它。
是的,2015 年,谷歌决定将 SPDY 合并到 HTTP 中,并推出了 HTTP/2,以避免出现两个竞争的标准。同时,他们废弃了 SPDY 协议。这样做的目的是为了统一协议,并将 SPDY 的优点融入到下一代 HTTP 协议中。HTTP/2 在设计上受到了 SPDY 的影响,并且在性能和效率方面有所改进。因此,HTTP/2 可以看作是 SPDY 的继任者。
HTTP/2 - 2015
到目前为止,您一定已经认识到为什么我们需要对 HTTP 协议进行另一次修订。HTTP/2 旨在实现内容的低延迟传输。与旧版本的 HTTP/1.1 相比,HTTP/2 的关键特性或区别包括:
- 二进制而不是文本化
- 多路复用 - 在单个连接上进行多个异步 HTTP 请求
- 使用 HPACK 进行头部压缩
- 服务器推送 - 单个请求的多个响应
- 请求优先级排序
- Security
1. 二进制协议
HTTP/2 旨在通过将其打造成二进制协议来解决 HTTP/1.x 中存在的延迟增加问题。作为二进制协议,它更易于解析,但与 HTTP/1.x 不同的是,它不再可读性强。HTTP/2 的主要构建模块是帧(Frames)
和流(Streams)
。
帧(Frames)和 流(Streams)
现在,HTTP 消息由一个或多个帧组成。其中,有一个用于元数据的 HEADERS 帧,一个用于有效载荷的 DATA 帧,还有其他几种类型的帧(如 HEADERS、DATA、RST_STREAM、SETTINGS、PRIORITY 等),你可以通过 HTTP/2 规范进行查阅。
每个 HTTP/2
请求和响应都被赋予一个唯一的流 ID
,并且它们被分割成帧
。帧实际上就是二进制数据片段
。一组帧
被称为流(Stream)
。每个帧都有一个流 ID
,用于标识它所属的流,并且每个帧都有一个公共头部。此外,除了流 ID 是唯一的之外,值得一提的是,由客户端发起的任何请求使用奇数流 ID
,而服务器的响应则使用偶数流 ID
。
除了 HEADERS
和 DATA
之外,另一个值得在这里提到的帧类型是 RST_STREAM
。RST_STREAM
是一种特殊的帧类型,用于中止某个流,即客户端可以发送此帧来告知服务器不再需要该流。在 HTTP/1.1
中,使服务器停止向客户端发送响应的唯一方法是关闭连接,这会导致增加延迟,因为每个连续请求都需要打开一个新连接。而在 HTTP/2
中,客户端可以使用 RST_STREAM 来停止接收特定的流,同时连接仍然保持打开状态,其他流仍然继续进行。
2. 多路复用(Multiplexing)
由于 HTTP/2 现在是一种二进制协议
,并且如我之前所说,它使用帧和流
来处理请求和响应,所以一旦 TCP 连接
建立,所有的流都通过同一个连接异步发送
,而无需打开额外的连接。服务器也以同样的异步方式
进行响应,即响应没有顺序,客户端使用分配的流 ID 来识别特定数据包所属的流。这也解决了 HTTP/1.x 中存在的排队阻塞问题
,即客户端不需要等待耗时的请求
,其他请求仍然可以被处理。
3. 头部压缩(Header Compression)
头部压缩是HTTP/2
的一个独立 RFC 的一部分,专门旨在优化发送的头部信息。其核心思想是,当我们从同一个客户端不断访问服务器时,我们会反复发送大量冗余的头部数据
,有时还会有增加头部大小的 Cookie
,这会增加带宽使用和延迟。为了解决这个问题,HTTP/2 引入了头部压缩机制。
与请求和响应不同,头部不是以gzip
或 compress
等格式进行压缩,而是采用了不同的机制进行头部压缩。该机制使用 Huffman 编码
对字面值进行编码,并由客户端和服务器维护一个头部表。客户端和服务器在后续请求中省略了重复的头部
(例如用户代理等),并使用双方维护的头部表进行引用。
在我们讨论头部时,让我在这里补充一下,除了添加了一些伪头部(即:method、scheme、host
和 path
),头部与 HTTP/1.1 中的头部仍然相同。
4. 服务器推送
服务器推送是 HTTP/2 的另一个重要功能,当服务器知道客户端将要请求某个资源时,可以在客户端请求之前主动将该资源推送给客户端,而无需客户端主动请求。例如,假设一个浏览器加载一个网页,它会解析整个页面以确定需要从服务器加载的远程内容,然后发送相应的请求到服务器获取这些内容。
服务器推送允许服务器通过推送预期客户端需要的数据来减少往返次数
。具体实现方式是,服务器发送一种特殊的帧称为 PUSH_PROMISE
,通知客户端说:“嘿,我将要发送这个资源给你!你不需要再请求它了
。” PUSH_PROMISE 帧与触发推送的流相关联,并包含了被承诺推送的流 ID,即服务器将要发送推送资源的流 ID。
5. 请求优先级化
客户端可以通过在打开流时在 HEADERS 帧
中包含优先级信息来为流分配优先级
。在任何其他时候,客户端可以发送一个 PRIORITY 帧
来更改流的优先级。
如果没有任何优先级信息,服务器会异步地处理请求,即没有任何顺序。如果为流分配了优先级,那么基于这个优先级信息,服务器决定分配多少资源来处理哪个请求。
6. 安全
关于是否应该将安全性(通过 TLS)
作为 HTTP/2 的强制要求进行了广泛讨论。最终决定不将其作为强制要求。然而,大多数供应商表示,只有在使用 TLS 时才支持 HTTP/2。因此,尽管 HTTP/2 规范上并不要求加密,但从默认情况来看,它已经成为一种默认的强制要求。既然这一点已经明确,当 HTTP/2 在 TLS 上实现时确实会有一些要求,比如必须使用 TLS 1.2 或更高版本,必须具备一定级别的最小密钥长度,需要使用临时密钥等。