5 分钟读透 HTTP 的前世今生

Posted by Mike on 2020-05-23

HTTP (Hypertext transfer protocol) 翻译成中文是超文本传输协议,是互联网上重要的一个协议。由欧洲核子研究委员会 CERN 的英国工程师 Tim Berners-Lee v 发明的,同时他也是 WWW 的发明人,最初的主要是用于传递通过 HTML 封装过的数据。在 1991 年发布了 HTTP 0.9 版,在 1996 年发布 1.0 版。1997 年是 1.1 版,1.1 版也是到今天为止传输最广泛的版本(初始 RFC 2068 在 1997 年发布, 然后在 1999 年被 RFC 2616 取代,再在 2014 年被 RFC 7230/7231/7232/7233/7234/7235 取代)。2015 年发布了 2.0 版,其极大的优化了 HTTP/1.1 的性能和安全性,而 2018 年发布的 3.0 版,继续优化 HTTP/2,激进地使用 UDP 取代 TCP 协议。目前,HTTP/3 在 2019 年 9 月 26 日 被 Chrome、Firefox、和 Cloudflare 支持。所以我想写下这篇文章,简单地说一下 HTTP 的前世今生,让大家学到一些知识,并希望可以在推动一下 HTTP 标准协议的发展。

HTTP 0.9 / 1.0

0.9 和 1.0 这两个版本,就是最传统的 Request – Response 的模式了。HTTP 0.9 版本的协议简单到极点,请求时不支持请求头,只支持 GET 方法,没了。HTTP 1.0 扩展了 0.9 版,其中主要增加了几个变化:

  • 在请求中加入了 HTTP 版本号,如:GET /coolshell/index.html HTTP/1.0

  • HTTP 开始有 Header了,不管是 Request 还是 Response 都有 Header 了。

  • 增加了 HTTP Status Code 标识相关的状态码。

  • 还有 Content-Type 可以传输其它的文件了。

我们可以看到,HTTP 1.0 开始让这个协议变得很文明了,一种工程文明。因为:

  • 一个协议有没有版本管理,是一个工程化的象征。

  • Header 可以说是把元数据和业务数据解耦,也可以说是控制逻辑和业务逻辑的分离。

  • Status Code 的出现可以让请求双方以及第三方的监控或管理程序有了统一的认识。最关键是还是控制错误和业务错误的分离。

注:国内很多公司 HTTP 无论对错只返回 200,这种把 HTTP Status Code 全部抹掉完全是一种工程界的倒退

但是,HTTP 1.0 性能上有一个很大的问题,那就是每请求一个资源都要新建一个 TCP 链接。而且是串行请求,所以就算网络变快了,打开网页的速度也还是很慢。所以,HTTP 1.0 应该是一个必须要淘汰的协议了。

HTTP/1.1

HTTP/1.1 主要解决了 HTTP 1.0 的网络性能的问题,以及增加了一些新的东西:

  • 可以设置 Keepalive 来让 HTTP 重用 TCP 链接,重用 TCP 链接可以省了每次请求都要在广域网上进行的 TCP 的三次握手的巨大开销。这是所谓的 “HTTP 长链接” 或是 “请求响应式的 HTTP 持久链接”。英文叫 HTTP Persistent Connection.

  • 然后支持 Pipeline 网络传输,只要第一个请求发出去了,不必等其回来,就可以发第二个请求出去,可以减少整体的响应时间。(注:非幂等的 POST 方法或是有依赖的请求是不能被 Pipeline 化的)

  • 支持 Chunked Responses,也就是说,在 Response 的时候,不必说明 Content-Length 这样,客户端就不能断连接,直到收到服务端的 EOF 标识。这种技术又叫 “服务端 Push 模型”,或是 “服务端 Push 式的 HTTP 持久链接

  • 还增加了 Cache Control 机制。

  • 协议头注增加了 Language、Encoding、Type 等等头,让客户端可以跟服务器端进行更多的协商。

  • 还正式加入了一个很重要的头 —— HOST 这样的话,服务器就知道你要请求哪个网站了。因为可以有多个域名解析到同一个 IP 上,要区分用户是请求的哪个域名,就需要在 HTTP 的协议中加入域名的信息,而不是被 DNS 转换过的 IP 信息。

  • 正式加入了 OPTIONS 方法,其主要用于 CORS – Cross Origin Resource Sharing 应用。

HTTP/1.1 应该分成两个时代,一个是 2014 年前,一个是 2014 年后。因为 2014 年 HTTP/1.1 有了一组 RFC(7230 /7231/7232/7233/7234/7235),这组 RFC 又叫 “HTTP/2 预览版”。其中影响 HTTP 发展的是两个大的需求:

  • 一个需要是加大了 HTTP 的安全性,这样就可以让 HTTP 应用得广泛。比如,使用 TLS 协议。

  • 另一个是让 HTTP 可以支持更多的应用,在 HTTP/1.1 下,HTTP 已经支持四种网络协议:

    • 传统的短链接。

    • 可重用 TCP 的的长链接模型。

    • 服务端 Push 的模型。

    • WebSocket 模型。

自从 2005 年以来,整个世界的应用 API 越来多,这些都造就了整个世界在推动 HTTP 的前进。我们可以看到,自 2014 的 HTTP/1.1 以来,这个世界基本的应用协议的标准基本上都是向 HTTP 看齐了。也许 2014 年前,还有一些专用的 RPC 协议。但是 2014 年以后,HTTP 协议的增强,让我们实在找不出什么理由不向标准靠拢,还要重新发明轮子了。

HTTP/2

虽然 HTTP/1.1 已经开始变成应用层通讯协议的一等公民了,但是还是有性能问题,虽然 HTTP/1.1 可以重用 TCP 链接,但是请求还是一个一个串行发的,需要保证其顺序。然而,大量的网页请求中都是些资源类的东西,这些东西占了整个 HTTP 请求中最多的传输数据量。所以,理论上来说,如果能够并行这些请求,那就会增加更大的网络吞吐和性能。

另外,HTTP/1.1 传输数据时,是以文本的方式。借助耗 CPU 的 Zip 压缩的方式减少网络带宽,但是耗了前端和后端的 CPU。这也是为什么很多 RPC 协议诟病 HTTP 的一个原因,就是数据传输的成本比较大。

其实,在 2010 年时,Google 就在搞一个实验型的协议,这个协议叫 SPDY。这个协议成为了 HTTP/2 的基础(也可以说成 HTTP/2 就是 SPDY 的复刻)。HTTP/2 基本上解决了之前的这些性能问题,其和 HTTP/1.1 最主要的不同是:

  • HTTP/2 是一个二进制协议,增加了数据传输的效率。

  • HTTP/2 是可以在一个 TCP 链接中并发请求多个 HTTP 请求,移除了 HTTP/1.1 中的串行请求。

  • HTTP/2 会压缩头,如果你同时发出多个请求,他们的头是一样的或是相似的。那么,协议会帮你消除重复的部分。这就是所谓的 HPACK 算法(参看 RFC 7541 附录 A)

  • HTTP/2 允许服务端在客户端放 Cache,又叫服务端 Push,也就是说,你没有请求的东西,我服务端可以先送给你放在你的本地缓存中。比如,你请求 X,我服务端知道 X 依赖于 Y,虽然你没有的请求 Y,但我把 Y 跟着 X 的请求一起返回客户端。

对于这些性能上的改善,在 Medium 上有篇文章 “ HTTP/2: the difference between HTTP/1.1, benefits and how to use it (https://url.cn/5Ij0hXz) ” 你可看一下相关的细节说明和测试。

当然,还需要注意到的是 HTTP/2 的协议复杂度比之前所有的 HTTP 协议的复杂度都上升了许多许多。其内部还有很多看不见的东西,比如其需要维护一个 “优先级树” 来用于来做一些资源和请求的调度和控制。如此复杂的协议,自然会产生一些不同的声音,或是降低协议的可维护和可扩展性。所以也有一些争议。尽管如此,HTTP/2 还是很快地被世界所采用。

HTTP/2 是 2015 年推出的。其发布后,Google 宣布移除对 SPDY 的支持,拥抱标准的 HTTP/2。过了一年后,就有 8.7% 的网站开启了 HTTP/2,根据这份报告 (https://url.cn/5YOuflM) ,截止至本文发布时(2019 年 10 月 1 日), 在全世界范围内已经有 41% 的网站开启了 HTTP/2。

HTTP/2 的官方组织在 Github 上维护了一份各种语言对 HTTP/2 的实现列表,大家可以去看看。

我们可以看到,HTTP/2 在性能上对 HTTP 有质的提高。所以,HTTP/2 被采用的也很快。如果你在你的公司内负责架构的话,HTTP/2 是你一个非常重要的需要推动的一个事。除了因为性能上的问题,推动标准落地也是架构师的主要职责。因为,你企业内部的架构越标准,你可以使用到开源软件,或是开发方式就会越有效率。跟随着工业界的标准的发展,你的企业会非常自然的享受到标准所带来的红利。

HTTP/3

然而,这个世界没有完美的解决方案。HTTP/2 也不例外,其主要的问题是:若干个 HTTP 的请求在复用一个 TCP 的连接,底层的 TCP 协议是不知道上层有多少个 HTTP 的请求的。所以,一旦发生丢包,造成的问题就是所有的 HTTP 请求都必需等待这个丢了的包被重传回来,哪怕丢的那个包不是我这个 HTTP 请求的。因为 TCP 底层是没有这个知识了。

这个问题又叫 Head-of-Line Blocking 问题,这也是一个比较经典的流量调度的问题。这个问题最早主要的发生的交换机上。下图来自 Wikipedia。

图中,左边的是输入队列。其中的 1、2、3、4 表示四个队列,四个队列中的 1、2、3、4 要去右边的 Output 的端口号。此时,第一个队列和第三个队列都要写右边的第四个端口。然后,一个时刻只能处理一个包。所以,一个队列只能在那等另一个队列写完。其此时的 3 号或 1 号端口是空闲的,而队列中的要去 1 和 3 号端口号的数据,被第四号端口给 Block 住了。这就是所谓的 HOL Blocking 问题。

HTTP/1.1 中的 Pipeline 中如果有一个请求 Block 了,那么队列后请求也统统被 Block 住了;HTTP/2 多请求复用一个 TCP 连接,一旦发生丢包就会 Block 住所有的 HTTP 请求。这样的问题很讨厌。好像基本无解了。

是的 TCP 是无解了,但是 UDP 是有解的 !于是 HTTP/3 破天荒地把 HTTP 底层的 TCP 协议改成了 UDP!

然后又是 Google 家的协议进入了标准 – QUIC (Quick UDP Internet Connections)。接下来是 QUIC 协议的几个重要的特性,为了讲清楚这些特性,我需要带着问题来讲(注:下面的网络知识,如果你看不懂的话,你需要学习一下 《TCP/IP 详解》 一书( 在我写 Blog 的这 15 年里,这本书推荐了无数次了),或是看一下本站的 《 TCP 的那些事》。):

  • 首先是上面的 Head-of-Line Blocking 问题,在 UDP 的世界中,这个就没了。这个应该比较好理解,因为 UDP 不管顺序,不管丢包(当然,QUIC 的一个任务是要像 TCP 的一个稳定,所以 QUIC 有自己的丢包重传的机制)

  • TCP 是一个无私的协议,也就是说,如果网络上出现拥塞,大家都会丢包,于是大家都会进入拥塞控制的算法中。这个算法会让所有人都 “冷静” 下来,然后进入一个 “慢启动” 的过程,包括在 TCP 连接建立时,这个慢启动也在,所以导致 TCP 性能迸发地比较慢。QUIC 基于 UDP,使用更为激进的方式。同时,QUIC 有一套自己的丢包重传和拥塞控制的协议,一开始 QUIC 是重新实现 TCP 的 CUBIC 算法。但是随着 BBR 算法的成熟(BBR 也在借鉴 CUBIC 算法的数学模型),QUIC 也可以使用 BBR 算法。这里,多说几句,**从模型来说,以前的 TCP 的拥塞控制算法玩的是数学模型,而新型的 TCP 拥塞控制算法是以 BBR 为代表的测量模型。**理论上来说,后者会更好,但 QUIC 的团队在一开始觉得 BBR 不如 CUBIC 的算法好,所以没有用。现在的 BBR 2.x 借鉴了 CUBIC 数学模型让拥塞控制更公平。这里有文章大家可以一读 “TCP BBR : Magic dust for network performance.” (https://url.cn/5awu1ey)

  • 接下来,现在要建立一个 HTTPS 的连接。先是 TCP 的三次握手,然后是 TLS 的三次握手,要整出六次网络交互,一个连接才建好。虽说 HTTP/1.1 和 HTTP/2 的连接复用解决这个问题,但是基于 UDP 后,UDP 也得要实现这个事。于是 QUIC 直接把 TCP 的和 TLS 的合并成了三次握手(对此,在 HTTP/2 的时候,是否默认开启 TLS 业内是有争议的。反对派说,TLS 在一些情况下是不需要的,比如企业内网的时候。而支持派则说,TLS 的那些开销,什么也不算了)。

所以,QUIC 是一个在 UDP 之上的伪 TCP + TLS + HTTP/2 的多路复用的协议。

但是对于 UDP 还是有一些挑战的,这个挑战主要来自互联网上的各种网络设备。这些设备根本不知道是什么 QUIC,他们看 QUIC 就只能看到的就是 UDP,所以,在一些情况下,UDP 就是有问题的。

  • 比如在 NAT 的环境下,如果是 TCP 话,NAT 路由或是代理服务器,可以通过记录 TCP 的四元组(源地址、源端口、目标地址、目标端口)来做连接映射的。然而,在 UDP 的情况下不行了。于是,QUIC 引入了个叫 Connection ID 的不透明的 ID 来标识一个链接,用这种业务 ID 很爽的一个事是,如果你从你的 3G/4G 的网络切到 WiFi网络(或是反过来),你的链接不会断,因为我们用的是 Connection ID,而不是四元组。

  • 然而就算引用了 Connection ID,也还是会有问题,比如一些不够 “聪明” 的等价路由交换机。这些交换机会通过四元组来做 Hash 把你的请求的 IP 转到后端的实际的服务器上。然而,他们不懂 Connection ID,只懂四元组。这么导致属于同一个 Connection ID 但是四元组不同的网络包就转到了不同的服务器上,这就是导致数据不能传到同一台服务器上,数据不完整,链接只能断了。所以,你需要更聪明的算法(可以参看 Facebook 的 Katran 开源项目 )

好了,就算搞定上面的东西,还有一些业务层的事没解。这个事就是 HTTP/2 的头压缩算法 HPACK,HPACK 需要维护一个动态的字典表来分析请求的头中哪些是重复的,HPACK 的这个数据结构需要在 Encoder 和 Decoder 端同步这个东西。在 TCP 上,这种同步是透明的,然而在 UDP 上这个事不好干了。所以,这个事也必需要重新设计了,基于 QUIC 的 QPACK 就出来了,利用两个附加的 QUIC Steam,一个用来发送这个字典表的更新给对方,另一个用来 Ack 对方发过来的 Update。

目前看下来,HTTP/3 目前看上去没有太多的协议业务逻辑上的东西,更多是 HTTP/2 + QUIC 协议。但 HTTP/3 因为动到了底层协议,所以,在普及方面上可能会比 HTTP/2 要慢的多的多。但是,可以看到 QUIC 协议的强大。细思及恐,QUIC 这个协议真对 TCP 是个威胁,如果 QUIC成熟了,TCP 是不是会有可能成为历史呢?

未来十年,让我们看看 UDP 是否能够逆袭 TCP……

来源:酷壳

原文:https://url.cn/56Z548W

题图:来自谷歌图片搜索

版权:本文版权归原作者所有

投稿:欢迎投稿,邮箱: editor@hi-linux.com