最近一段时间以来,关于HTTP/3的新闻有很多,越来越多的国际大公司已经开始使用HTTP/3了。
所以,HTTP/3已经是箭在弦上了,全面使用只是个时间问题,那么,作为一线开发者,我们也是时候了解下到底什么是HTTP/3,为什么需要HTTP/3了。
那么,本文就来讲解一下到底什么是HTTP/3?他用到了哪些技术?解决了什么问题?
先介绍下HTTP 的前世今生
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 标准协议的发展。
1
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 应该是一个必须要淘汰的协议了。
2
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 协议的增强,让我们实在找不出什么理由不向标准靠拢,还要重新发明轮子了。
3
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/2 存在的问题
HTTP/2因为底层使用的传输层协议仍然是TCP,所以他存在着TCP队头阻塞、TCP握手延时长以及协议僵化等问题。
这导致HTTP/2虽然使用了多路复用、二进制分帧等技术,但是仍然存在着优化空间。
QUIC协议
我们知道,HTTP/2之所以"被弃用",是因为他使用的传输层协议仍然是TCP,所以HTTP/3首要解决的问题就是绕开TCP。
那么如果研发一种新的协议,同样还是会因为受到中间设备僵化的影响,导致无法被大规模应用。所以,研发人员们想到了一种基于UDP实现的方式。
于是,Google是最先采用这种方式并付诸于实践的,他们在2013年推出了一种叫做QUIC的协议,全称是Quick UDP Internet Connections。
从名字中可以看出来,这是一种完全基于UDP的协议。
在设计之初,Google就希望使用这个协议来取代HTTPS/HTTP协议,使网页传输速度加快。2015年6月,QUIC的网络草案被正式提交至互联网工程任务组。2018 年 10 月,互联网工程任务组 HTTP 及 QUIC 工作小组正式将基于 QUIC 协议的 HTTP(英语:HTTP over QUIC)重命名为HTTP/3。
所以,我们现在所提到的HTTP/3,其实就是HTTP over QUIC,即基于QUIC协议实现的HTTP。
那么,想要了解HTTP/3的原理,只需要了解QUIC就可以了。
QUIC协议有以下特点:
基于UDP的传输层协议:它使用UDP端口号来识别指定机器上的特定服务器。
可靠性:虽然UDP是不可靠传输协议,但是QUIC在UDP的基础上做了些改造,使得他提供了和TCP类似的可靠性。它提供了数据包重传、拥塞控制、调整传输节奏以及其他一些TCP中存在的特性。
实现了无序、并发字节流:QUIC的单个数据流可以保证有序交付,但多个数据流之间可能乱序,这意味着单个数据流的传输是按序的,但是多个数据流中接收方收到的顺序可能与发送方的发送顺序不同!
快速握手:QUIC提供0-RTT和1-RTT的连接建立
使用TLS 1.3传输层安全协议:与更早的TLS版本相比,TLS 1.3有着很多优点,但使用它的最主要原因是其握手所花费的往返次数更低,从而能降低协议的延迟。
那么,QUIC到底属于TCP/IP协议族中的那一层呢?我们知道,QUIC是基于UDP实现的,并且是HTTP/3的所依赖的协议,那么,按照TCP/IP的分层来讲,他是属于传输层的,也就是和TCP、UDP属于同一层。
如果更加细化一点的话,因为QUIC不仅仅承担了传输层协议的职责,还具备了TLS的安全性相关能力,所以,可以通过下图来理解QUIC在HTTP/3的实现中所处的位置。
接下来我们分别展开分析一下QUIC协议。先来看下他是如何建立连接的。
QUIC的连接建立
我们知道,TCP这种可靠传输协议需要进行三次握手,也正是因为三次握手,所以需要额外消耗1.5 RTT,而如果再加上TLS的话,则需要消耗3-4个 RTT连接。
那么,QUIC是如何建立连接的呢?如何减少RTT的呢?
QUIC提出一种新的连接建立机制,基于这种连接接机制,实现了快速握手功能,一次QUIC连接建立可以实现使用 0-RTT 或者 1-RTT 来建立连接。
QUIC在握手过程中使用Diffie-Hellman算法来保证数据交互的安全性并合并了它的加密和握手过程来减小连接建立过程中的往返次数。
Diffie–Hellman (以下简称DH)密钥交换是一个特殊的交换密钥的方法。它是密码学领域内最早付诸实践的密钥交换方法之一。 DH可以让双方在完全缺乏对方(私有)信息的前提条件下通过不安全的信道达成一个共享的密钥。此密钥用于对后续信息交换进行对称加密。
QUIC 连接的建立整体流程大致为:QUIC在握手过程中使用Diffie-Hellman算法协商初始密钥,初始密钥依赖于服务器存储的一组配置参数,该参数会周期性的更新。初始密钥协商成功后,服务器会提供一个临时随机数,双方根据这个数再生成会话密钥。客户端和服务器会使用新生的的密钥进行数据加解密。
以上过程主要分为两个步骤:初始握手(Initial handshake)、最终(与重复)握手(Final (and repeat) handshake),分别介绍下这两个过程。
初始握手(Initial handshake)
在连接开始建立时,客户端会向服务端发送一个打招呼信息,(inchoate client hello (CHLO)),因为是初次建立,所以,服务端会返回一个拒绝消息(REJ),表明握手未建立或者密钥已过期。
但是,这个拒绝消息中还会包含更多的信息(配置参数),主要有:
Server Config:一个服务器配置,包括服务器端的Diffie-Hellman算法的长期公钥(long term Diffie-Hellman public value)
Certificate Chain:用来对服务器进行认证的信任链
Signature of the Server Config:将Server Config使用信任链的叶子证书的public key加密后的签名
Source-Address Token:一个经过身份验证的加密块,包含客户端公开可见的IP地址和服务器的时间戳。
在客户端接收到拒绝消息(REJ)之后,客户端会进行数据解析,签名验证等操作,之后会将必要的配置缓存下来。
同时,在接收到REJ之后,客户端会为这次连接随机产生一对自己的短期密钥(ephemeral Diffie-Hellman private value) 和 短期公钥(ephemeral Diffie-Hellman public value)。
之后,客户端会将自己刚刚产生的短期公钥打包一个Complete CHLO的消息包中,发送给服务端。这个请求的目的是将自己的短期密钥传输给服务端,方便做前向保密,后面篇幅会详细介绍。

在发送了Complete CHLO消息给到服务器之后,为了减少RTT,客户端并不会等到服务器的响应,而是立刻会进行数据传输。
为了保证数据的安全性,客户端会自己的短期密钥和服务器返回的长期公钥进行运算,得到一个初始密钥(initial keys)。
有了这个初识密钥之后,客户端就可以用这个密钥,将想要传输的信息进行加密,然后把他们安全的传输给服务端了。
另外一面,接收到Complete CHLO请求的服务器,解析请求之后,就同时拥有了客户端的短期公钥和自己保存的长期密钥。这样通过运算,服务端就能得到一份和客户端一模一样的初始密钥(initial keys)。
接下来他接收到客户端使用初始密钥加密的数据之后,就可以使用这个初识密钥进行解密了,并且可以将自己的响应再通过这个初始密钥进行加密后返回给客户端。
所以,从开始建立连接一直到数据传送,只消耗了初始连接连接建立的 1 RTT
最终(与重复)握手
那么,之后的数据传输就可以使用初始密钥(initial keys)加密了吗?
其实并不完全是,因为初始密钥毕竟是基于服务器的长期公钥产生的,而在公钥失效前,几乎多有的连接使用的都是同一把公钥,所以,这其实存在着一定的危险性。
所以,为了达到前向保密 (Forward Secrecy) 的安全性,客户端和服务端需要使用彼此的短期公钥和自己的短期密钥来进行运算。
在密码学中,前向保密(英语:Forward Secrecy,FS)是密码学中通讯协议的安全属性,指的是长期使用的主密钥泄漏不会导致过去的会话密钥泄漏。
那么现在问题是,客户端的短期密钥已经发送给服务端,而服务端只把自己的长期密钥给了客户端,并没有给到自己的短期密钥。
所以,服务端在收到Complete CHLO之后,会给到服务器一个server hello(SHLO)消息,这个消息会使用初始密钥(initial keys)进行加密。
这个CHLO消息包中,会包含一个服务端重新生成的短期公钥。
这样客户端和服务端就都有了对方的短期公钥(ephemeral Diffie-Hellman public value)。
这样,客户端和服务端都可以基于自己的短期密钥和对方的短期公钥做运算,产生一个仅限于本次连接使用的前向保密密钥 (Forward-Secure Key),后续的请求发送,都基于这个密钥进行加解密就可以了。
这样,双方就完成了最终的密钥交换、连接的握手并且建立了QUIC连接。
当下一次要重新创建连接的时候,客户端会从缓存中取出自己之前缓存下来的服务器的长期公钥,并重新创建一个短期密钥,重新生成一个初识密钥,再使用这个初始密钥对想要传输的数据进行加密,向服务器发送一个Complete CHLO 请求即可。这样就达到了0 RTT的数据传输。
所以,如果是有缓存的长期公钥,那么数据传输就会直接进行,准备时间是0 RTT
以上,通过使用Diffie-Hellman算法协商密钥,并且对加密和握手过程进行合并,大大减小连接过程的RTT ,使得基于QUIC的连接建立可以少到1 RTT甚至0 RTT。
以下,是Google官网上面的一张关于QUIC连接建立的流程图,可以帮助大家理解这个过程。
另外,通过以上关于握手建立的过程,我们也可以知道,QUIC在整个过程中通过加解密的方式很好的保证了安全性。
多路复用
基于TCP的协议实现的HTTP有一个最大的问题那就是队头阻塞问题,那么,在这方面,QUIC是如何解决这个问题的呢?
TCP传输过程中会把数据拆分为一个个按照顺序排列的数据包,这些数据包通过网络传输到了接收端,接收端再按照顺序将这些数据包组合成原始数据,这样就完成了数据传输。
但是如果其中的某一个数据包没有按照顺序到达,接收端会一直保持连接等待数据包返回,这时候就会阻塞后续请求。这就发生了TCP队头阻塞。
类似于HTTP/2,QUIC在同一物理连接上可以有多个独立的逻辑数据流,这些数据流并行在同一个连接上传输,且多个数据流之间间的传输没有时序性要求,也不会互相影响。
数据流(Streams)在QUIC中提供了一个轻量级、有序的字节流的抽象化
QUIC的单个数据流可以保证有序交付,但多个数据流之间可能乱序。这意味着单个数据流的传输是按序的,但是多个数据流中接收方收到的顺序可能与发送方的发送顺序不同!
也就是说同一个连接上面的多个数据流之间没有任何依赖(不要求按照顺序到达),即使某一个数据包没有达到,也只会影响自己这个数据流,并不会影响到到其他的数据流。
连接迁移
对于TCP连接的识别,需要通过服务器和客户端过双方的ip和端口四个参数进行的。在网络切换的场景中,比如手机切换网络,那么自身的ip就会发生变化。这就导致之前的TCP连接就会失效,就需要重新建立。
这种场景对于移动端设备普及的今天来说,还是比较频繁的。
所以,在这一点上,QUIC进行了优化。
QUIC协议使用特有的UUID来标记每一次连接,在网络环境发生变化的时候,只要UUID不变,就能不需要握手,继续传输数据。
可靠性
TCP之所以被称之为可靠链接,不仅仅是因为他有三次握手和四次关闭的过程,还因为他做了很多诸如流量控制、数据重传、拥塞控制等可靠性保证。
这也是为什么一直以来都是以TCP作为HTTP实现的重要协议的原因。
那么,QUIC想要取代TCP,就需要在这方面也做出努力,毕竟UDP自身是不具备这些能力的。
TCP拥塞控制是TCP避免网络拥塞的算法,是互联网上主要的一个拥塞控制措施。经典的算法实现有很多,诸如TCP Tahoe 和 Reno、TCP Vegas、TCP Hybla、TCP New Reno、TCP Westwood和Westwood+以及TCP BIC 和 CUBIC等等。
QUIC协议同样实现了拥塞控制。不依赖于特定的拥塞控制算法,并且提供了一个可插拔的接口,允许用户实验。默认使用了 TCP 协议的 Cubic 拥塞控制算法。
关于流量控制,QUIC提供了基于stream和connection两种级别的流量控制,既需要对单个 Stream 进行控制,又需要针对所有 Stream 进行总体控制。
QUIC的连接级流控,用以限制 QUIC 接收端愿意分配给连接的总缓冲区,避免服务器为某个客户端分配任意大的缓存。连接级流控与流级流控的过程基本相同,但转发数据和接收数据的偏移限制是所有流中的总和。
弊端
以上,我们介绍了很多QUIC的相比较于TCP的优点,可以说这种协议相比较于TCP确实要优秀一些。
因为他是基于UDP的,并没有改变UDP协议本身,只是做了一些增强,虽然可以避开中间设备僵化的问题,但是,在推广上面也不是完全没有问题的。
首先,很多企业、运营商和组织对53端口(DNS)以外的UDP流量会进行拦截或者限流,因为这些流量近来常被滥用于攻击。
特别是一些现有的UDP协议和实现易受放大攻击(amplification attack)威胁,攻击者可以控制无辜的主机向受害者投放发送大量的流量。
所以,基于UDP的QUIC协议的传输可能会受到屏蔽。
另外,因为UDP一直以来定位都是不可靠连接,所以有很多中间设备对于他的支持和优化程度并不高,所以,出现丢包的可能性还是比较搞的。
总结
下表是我总结的HTTP/2和HTTP/3的异同点,有一些本文介绍过,有一些个人认为并不是特别重要的,本文中并没有提及,大家感兴趣的可以自行学习下。
特性 | HTTP/2 | HTTP/3 |
---|---|---|
传输层协议 | TCP | 基于UDP的QUIC |
默认加密 | 否 | 是 |
独立的数据流 | 否 | 是 |
队头阻塞 | 存在TCP队头阻塞 | 无 |
报头压缩 | HPACK | QPACK |
握手时延 | TCP+TLS 的 1-3 RTT | 0-1 RTT |
连接迁移 | 无 | 有 |
服务器推送 | 有 | 有 |
多路复用 | 有 | 有 |
流量控制 | 有 | 有 |
数据重传 | 有 | 有 |
拥塞控制 | 有 | 有 |
作者:HollisChuang
共同学习,写下你的评论
评论加载中...
作者其他优质文章