Journey to HTTP/2 – Kamran Ahmed
我最近太忙了,没时间更新博客。今天终于有时间了,想写一些关于 HTTP 的东东。
HTTP 是每个 Web 开发者都应该知道的协议,因为它为整个 Web 提供动力,知道它肯定会有助于开发出更好的应用程序。
在本文中,我将讨论 HTTP 是什么、如何产生的、现在的情况以及我们是如何到达这一步的。
What is HTTP?
首先,什么是 HTTP?HTTP 是基于TCP/IP的应用层通信协议,它标准化了客户端和服务器之间的通信方式,定义了如何通过 Internet 请求和传输内容。通过应用层协议,我的意思是它只是一个抽象层,它标准化主机(客户端和服务器)如何通信,它本身依赖于TCP/IP在客户端和服务器之间获取请求和响应。默认情况下使用 TCP 端口80,但也可以使用其他端口。但是,HTTPS (默认)使用端口443。
HTTP/0.9 - The One Liner (1991)
HTTP 的第一个文档版本是HTTP/0.9 在 1991 年提出的。它是有史以来最简单的协议。有一个名为GET的方法. 如果客户端必须访问服务器上的某个网页,它会发出如下简单请求
GET /index.html
来自服务器的响应如下所示
(response body) (connection closed)
也就是说,服务器将收到请求,并以 HTML 作为响应进行回复,一旦内容传输完成,连接将被关闭。此方法:
- 没有标题
- GET 是唯一允许的方法
- 响应必须是 HTML
如您所见,该协议实际上只不过是即将发生的事情的垫脚石。
HTTP/1.0 - 1996
1996 年,HTTP 的下一个版本 即HTTP/1.0 比原来的版本有了很大的改进。
与HTTP/0.9只有 HTML 响应的设计不同,HTTP/1.0现在可以处理其他响应格式,即图像、视频文件、纯文本或任何其他内容类型。它添加了更多方法(即POST和HEAD),请求/响应格式发生了变化,HTTP 标头添加到请求和响应中,添加了状态代码来标识响应,引入了字符集支持,多部分类型,授权,缓存, 内容编码等等。
以下是 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(HTTP 后跟版本号),然后是状态码200,后跟原因短语(或者状态码的描述,如果你愿意的话)。
在这个较新的版本中,请求和响应标头仍然保持ASCII编码,但响应主体可以是任何类型,即图像、视频、HTML、纯文本或任何其他内容类型。所以,现在服务器可以向客户端发送任何内容类型;(新版本)引入后不久,在HTTP中的“超文本”一词就变得用词不当。HMTP或超媒体传输协议可能更有意义,但我想,我们永远都会被这个名字所困扰。
HTTP/1.0的主要缺点之一是每个连接不能有多个请求。也就是说,每当客户端需要来自服务器的某些东西时,它都必须打开一个新的 TCP 连接,并且在满足该单个请求后,连接将被关闭。对于任何下一个要求,它必须建立在新的连接上。为什么不好?好吧,让我们假设您访问的网页包含10个图像、5个样式表和5个javascript 文件,在向该网页发出请求时总计20个东西需要获取。由于服务器在请求完成后立即关闭连接,因此会出现20个单独的连接,其中每个东西将在其单独的连接上一个接一个地提供服务。如此大量的连接会导致严重的性能损失,因为新TCP连接会因三次握手和慢启动而导致显著的性能损失。
Three-way Handshake
简单形式的三次握手是所有TCP连接都以三次握手(客户端和服务器在开始共享应用程序数据之前共享一系列数据包)开始。
- SYN - 客户端生成一个随机数,比如说x,并将其发送到服务器。
- SYN ACK - 服务器通过向客户端发送一个由随机数(比如说y) 和x+1(其中x是客户端发送的数字)组成的数据包来确认请求。
- ACK - 客户端增加y从服务器接收到的数字,并用 y+1 发回一个数据包。
一旦三次握手完成,客户端和服务器之间的数据共享就可以开始了。应该注意的是,客户端可能会在它分派最后一个ACK数据包后立即开始发送应用程序数据,但服务器仍然必须等待ACK数据包被接收才能完成请求。
然而,一些 HTTP/1.0的实现尝试通过引入一个名为 Connection: keep-alive 的新标头来克服此问题,该标头旨在告诉服务器“嘿服务器,不要关闭此连接,我还需要它”。但是,它并没有得到广泛的支持,问题仍然存在。
除了无连接之外,HTTP也是一个无状态协议,即服务器不维护有关客户端的信息,因此每个请求都必须具有服务器自己完成请求所需的信息,而不与任何旧请求有任何关联. 所以这简直是火上浇油,即除了客户端必须打开的大量连接之外,它还必须在线路上发送一些冗余数据,从而导致带宽使用增加。
HTTP/1.1 - 1999
仅仅用了3年,下一个版本 HTTP/1.1于 1999 年发布;这比它的前身做了很多改进,主要有:
- 新的 HTTP 方法,其中引入了PUT, PATCH, OPTIONS,DELETE
- 标头中的主机名标识 HTTP/1.0中 Host 不是必需的,但在HTTP/1.1中是必需的。
- 持久连接 如上所述,HTTP/1.0每个连接只有一个请求,并且在请求完成后立即关闭连接,这导致了严重的性能损失和延迟问题。HTTP/1.1引入了持久连接,即默认情况下连接没有关闭,而是保持打开状态,允许多个顺序请求。要关闭连接,请求中必须要有请求头Connection: close。客户端通常在最后一个请求中发送此标头以安全关闭连接。
- Pipelining 它还引入了对Pipelining的支持,客户端可以向服务器发送多个请求,而无需等待服务器上的同一连接的响应,并且服务器必须按照接收请求的相同顺序发送响应。但是客户端怎么知道何时第一个响应下载完成可以开始下一个响应。好吧,为了解决这个问题,必须使用Content-Length标头,客户端可以用它来识别响应在哪里结束且可以开始等待下一个响应。
需要注意的是,为了从持久连接或Pipelining中受益,响应中必须有Content-Length标头,因为这会让客户端知道传输何时完成并且可以发送下一个请求(以正常的顺序发送请求的方式)或开始等待下一个响应(启用Pipelining时)。
但是这种方法仍然存在问题。也就是说,如果数据是动态的并且服务器无法事先找到内容长度怎么办?那么在那种情况下,你真的不能从持久连接中受益,不是吗?!为了解决这个问题,HTTP/1.1引入了分块编码。在这种情况下,服务器可能会省略 content-Length 以支持分块编码(稍后会详细介绍)。但是,如果它们都不可用,则必须在请求结束时关闭连接。
- 分块传输 在动态内容的情况下,当传输开始,服务器无法真正找到Content-Length时,它可能会开始分块(逐块)发送内容,并在发送时为每个块添加Content-Length。并且当所有块都发送完毕,即整个传输完成时,它会发送一个空块,即Content-Length设置为零的块,以告诉客户端传输已完成。为了通知客户端有关分块传输,服务器需要包含标头Transfer-Encoding: chunked
- 与HTTP/1.0仅具有基本身份验证的不同,HTTP/1.1包括摘要和代理身份验证
- 缓存
- 字节范围
- 字符集
- 语言协商
- 客户端 cookie
- 增强的压缩支持
- 新状态码
- ..和更多
我不会在这篇文章中详细介绍 HTTP/1.1 的所有功能,因为它本身就是一个主题,你已经可以找到很多关于它的信息。我建议您阅读的一份文档是 HTTP/1.0 和HTTP/1.1之间的关键区别,这里是给优秀学生的原始RFC链接。
HTTP/1.1 于 1999 年推出,多年来一直是标准。虽然它比以前有了很大的改进;但随着网络每天都在变化,它开始变得陈旧。如今,加载网页比以往任何时候都更加耗费资源,一个简单的网页必须打开 30 多个连接。既然HTTP/1.1有持久连接,那为什么有这么多连接呢?啊!原因是,HTTP/1.1 它在任何时候都只能有一个未完成的连接。它试图通过引入Pipelining来解决这个问题,但由于线头阻塞 HTTP/1.1,它并没有完全解决这个问题,因为缓慢或繁重的请求可能会阻塞后面的请求,一旦请求卡在管道中,它将不得不等待该请求完成。为了克服 HTTP/1.1 这些缺点,开发人员开始实施变通方案,例如使用 spritesheets(雪碧图)、将图像编码到CSS、单个的巨大CSS/Javascript 文件、域名分片等。
SPDY - 2009
谷歌继续前进并开始实验替代的协议以提高网络速度和安全性,同时减少网页延迟。2009 年,他们宣布了SPDY.
SPDY是 Google 的商标,不是首字母缩略词。
可以看出,如果我们不断增加带宽,一开始网络性能会提高,但到了一个点,性能就提升不大了。但是如果你对延迟做同样的事情,即如果我们不断降低延迟,就会有持续的性能提升。这是SPDY背后性能提升的核心思想,减少延迟以提高网络性能。
对于那些不知道区别的人来说,延迟即数据在源和目标之间传输需要多长时间(以毫秒为单位),而带宽是每秒传输的数据量(比特/秒)。
SPDY包含多路复用、压缩、优先级、安全性等特性。我不打算深入讨论 SPDY 的细节,因为当我们在下一节触及 HTTP/2的本质时,你会明白这一点,正如我所说HTTP/2主要是受到SPDY的启发。
SPDY并没有真正尝试替换 HTTP;它是 HTTP 上的一个转换层,存在于应用层,并在将请求发送到网络之前对其进行了修改。它开始成为事实上的标准,并且大多数浏览器开始实施它。
2015年,Google不想有两个相互竞争的标准,因此他们决定将其合并到 HTTP 中,同时产生HTTP/2和弃用 SPDY。
HTTP/2 - 2015
到现在为止,您一定确信了为什么我们需要对 HTTP 协议进行另一次修订。HTTP/2专为内容的低延迟传输而设计。它的主要功能或与旧版本的区别包括:
- 二进制而不是文本
- 多路复用 - 通过单个连接的多个异步 HTTP 请求
- 使用 HPACK 压缩标头
- 服务器推送 - 单个请求的多个响应
- 请求优先级
- 安全
1. Binary Protocol
HTTP/2倾向于通过使其成为二进制协议来解决 HTTP/1.x 中存在的延迟增加的问题。作为二进制协议,它更易于解析,但与HTTP/1.x不同的是人眼不再可读。HTTP/2主要的构建块是帧和流。
帧和流
HTTP 消息现在由一个或多个帧组成。有一个用于元数据的HEADERS帧和一个用于有效负载的DATA帧,还有其他几种类型的帧(RST_STREAM、SETTINGS等PRIORITY),具体可以查看HTTP/2规范。
每个HTTP/2请求和响应都被赋予一个唯一的流 ID,并被划分为若干帧。帧只不过是二进制数据。帧的集合称为流。每个帧都有一个流 id,用于标识它所属的流,并且每个帧都有一个公共标头。另外,流ID除了是唯一的,还值得一提的是,客户端发起的任何请求都使用奇数流ID,而服务器的响应是偶数流ID。
除了HEADERS帧和DATA帧,另一种值得一提的特殊的帧类型RST_STREAM,用于中止某些流,即客户端可能会发送此帧以让服务器知道我不再需要此流。HTTP/1.1使服务器停止向客户端发送响应的唯一方法是关闭连接,这会导致延迟增加,因为必须为任何随后的请求打开一个新连接。在 HTTP/2 中,客户端可以使用RST_STREAM停止接收特定流,而连接仍将打开,其他流仍将正常工作。
2. Multiplexing
由于HTTP/2现在是二进制协议,并且正如我上面所说,它使用帧和流进行请求和响应,一旦打开 TCP 连接,所有流都通过同一连接异步发送,而无需打开任何其他连接。反过来,服务器以相同的异步方式响应,即响应没有顺序,客户端使用分配的流id 来识别特定数据包所属的流。这也解决了HTTP/1.x 中存在的行头阻塞问题,即客户端不必等待耗时的请求,其他请求仍将得到处理。
3. HPACK Header Compression
它是一个单独的专用于优化发送的标头的RFC的一部分。它的本质是,当我们不断地从同一个客户端访问服务器时,会一遍又一遍地在标头中发送大量冗余数据,有时还可能会有 cookie 增加标头大小,从而导致带宽使用和延迟增加。为了克服这个问题,HTTP/2引入了标头压缩。
与请求和响应不同,标头不会以gzip或compress其他格式压缩,而是有一种不同的标头压缩机制,即使用霍夫曼代码对字面值进行编码,并且由客户端和服务器维护一个标头表,客户端和服务器在后续请求中省略任何重复的标头(例如用户代理等),并使用标头表引用它们。
提到标头,这里补充一下,标头仍然与 HTTP/1.1 中的相同,除了添加了一些伪标头,即:method
, :scheme
, :host
, :path
4. Server Push
服务器推送是HTTP/2另一个极好的功能——服务器知道客户端将请求某个资源,甚至都不需要客户端请求它,就可以将其推送到客户端。例如(在不打开或没有此功能时),假设浏览器加载一个网页,它解析整个页面以找出它必须从服务器加载的远程内容,然后向服务器发送后续请求以获取该内容。
服务器推送允许服务器通过推送它知道客户端即将需要的数据来减少往返。它是如何完成的?服务器发送一个特殊的帧,称为PUSH_PROMISE通知客户端,“嘿,我要把这个资源发送给你!不要问我了。” PUSH_PROMISE帧与导致推送发生的流相关联,它包含promised的流ID,即服务器将在此流上发送要推送的资源。这里比较抽象,具体点的文章:
HTTP/2 服务器推送(Server Push)教程 - 阮一峰的网络日志
Using HTTP/2 Server Push with PHP
5. Request Prioritization
客户端可以通过在打开流的HEADERS帧中包含优先级信息来为流分配优先级。客户端随时可以发送PRIORITY帧来更改流的优先级。
没有优先级信息,服务器异步处理请求,即没有任何顺序。如果为流分配了优先级,则基于此优先级信息,服务器决定需要给予多少资源来处理哪个请求。
6. Security
关于HTTP/2是否应该强制使用安全性(通过TLS)进行了广泛的讨论。最后,决定不强制执行。但是,大多数供应商都表示,他们只会在使用TLS时才支持HTTP/2。因此,虽然按规范HTTP/2不需要进行加密,但无论如何默认情况下加密已经成为强制性的了。有了这个,当使用TLS时HTTP/2确实会强加一些要求:必须使用TLS1.2或更高版本,必须有一定程度的最小密钥大小,需要临时密钥等。
HTTP/2就在这里,它的适应能力已经超过了逐渐增加的SPDY。HTTP/2在性能增益方面有很多提升,现在是我们应该开始使用它的时候了。
对于任何对更多细节感兴趣的人,这里是规范和HTTP/2性能提升的展示.