应用层和传输层协议的历代优化
[TOC]
背景
影响一个HTTP网络请求的因素主要有两个:带宽和延迟
1.带宽
如果说我们还停留在拨号上网的阶段,带宽可能会成为一个比较严重影响请求的问题,但是现在网络基础建设已经使得带宽得到极大的提升,我们不再会担心由带宽而影响网速,那么就只剩下延迟了。
2.延迟
浏览器阻塞(HOL blocking):浏览器会因为一些原因阻塞请求。浏览器对于同一个域名,同时只能有 4 个连接(这个根据浏览器内核不同可能会有所差异),超过浏览器最大连接数限制,后续请求就会被阻塞。 DNS 查询(DNS Lookup):浏览器需要知道目标服务器的 IP 才能建立连接。将域名解析为 IP 的这个系统就是 DNS。这个通常可以利用DNS缓存结果来达到减少这个时间的目的。 建立连接(Initial connection):HTTP 是基于 TCP 协议的,浏览器最快也要在第三次握手时才能捎带 HTTP 请求报文,达到真正的建立连接,但是这些连接无法复用会导致每次请求都经历三次握手和慢启动。三次握手在高延迟的场景下影响较明显,慢启动则对文件类大请求影响较大。
HTTP1.0和HTTP1.1的一些区别
缓存处理(modified-since、etag)
在HTTP1.0中主要使用header里的If-Modified-Since,Expires来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag,If-Unmodified-Since, If-Match, If-None-Match等更多可供选择的缓存头来控制缓存策略。(在SDWebImage中旧址新图就使用了E-Tag值来保证图片是否需要更新)。
带宽优化及网络连接的使用(断点续传)
HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range头域,它允许只请求资源的某个部分,即返回码是206(Partial Content),这样就方便了开发者自由的选择以便于充分利用带宽和连接。
错误通知的管理(新增状态码)
在HTTP1.1中新增了24个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。
Host头处理
host字段可以是域名,也可以是ip地址。host字段域名/ip后可以跟端口号,如Host: www.6san.com:8080。
在HTTP1.0中认为每台服务器都绑定一个唯一的IP地址,因此,请求消息中的URL并没有传递主机名(hostname),可以缺失host字段。但随着虚拟主机技术的发展,在一台物理服务器上可以存在多个虚拟主机(Multi-homed Web Servers),并且它们共享一个IP地址。
HTTP1.1的请求消息和响应消息都应支持Host头域,且请求消息中如果没有Host头域会报告一个错误(400 Bad Request),但可以是空值。
长连接(keep-alive)
HTTP 1.1支持长连接(PersistentConnection)和请求的流水线(Pipelining)处理,在一个TCP连接上可以传送多个HTTP请求和响应,减少了建立和关闭连接的消耗和延迟,在HTTP1.1中默认开启Connection: keep-alive,一定程度上弥补了HTTP1.0每次请求都要创建连接的缺点。
HTTP1.0和1.1现存的一些问题
上面提到过的,HTTP1.x在传输数据时,每次都需要重新建立连接,无疑增加了大量的延迟时间,特别是在移动端更为突出。
虽然HTTP1.x支持了keep-alive,来弥补多次创建连接产生的延迟,但是keep-alive使用多了同样会给服务端带来大量的性能压力,并且对于单个文件被不断请求的服务(例如图片存放网站),keep-alive可能会极大的影响性能,因为它在文件被请求之后还保持了不必要的连接很长时间。
HTTP1.x在传输数据时,所有传输的内容都是明文,客户端和服务器端都无法验证对方的身份,这在一定程度上无法保证数据的安全性。
HTTP1.x在使用时,header里携带的内容过大,在一定程度上增加了传输的成本,并且每次请求header基本不怎么变化,尤其在移动端增加用户流量。
HTTPS
最初,HTTPS是与SSL一起使用的;在SSL逐渐演变到TLS时(其实两个是一个东西,只是名字不同而已),最新的HTTPS也由在2000年五月公布的RFC 2818正式确定下来。
简单来说,HTTPS就是安全版的HTTP,并且由于当今时代对安全性要求更高,chrome和firefox都大力支持网站使用HTTPS,苹果也在iOS 10系统中强制app使用HTTPS来传输数据,由此可见HTTPS势在必行。
HTTPS与HTTP的一些区别
HTTPS在运输层和应用层之间多了一层SSL(安全套接字层协议),目的是将数据进行加密处理和传输。端口号也从80变成了443。在进行三次握手后,还要进行SLL握手,双方进行非对称加密,协商出一个密钥,随后双方使用这个密钥进行对称加密传输数据。具体过程:
客户端将自己所拥有的加密算法发送给服务器,服务器选择其中一个非对称加密算法并将私钥保留,公钥放在证书机构请求的证书里(证书里也包含了服务器的身份信息等)一起发送给客户端,客户端使用本地的证书机构公钥解开证书得到被证书机构加密的公钥,拿到了公钥。客户端此时产生一段随机密码,作为以后对称加密的密钥。发送三段信息,一是用公钥加密这个随机密码,二是用随机密码加密一段报文,三是对报文hash化后的值。三段数据发送过去后,服务器用私钥得到随机密码,用随机密码加密了报文,对报文hash化,如果与发送过来的结果都一致,说明没有被篡改,这时双方就可以进行对称加密传输数据了。
使用SPDY加快你的网站速度
HTTP1.1将connection默认值刚才改为了keep-alive,这样同一个域名下的多个HTTP请求就可以复用同一个TCP连接,这种做法称之为HTTP Pipeline。这种做法明显的减少了连接次数,减少了TCP连接的资源浪费,但是像队头拥塞这样的问题还是没有解决。
假设加载一个HTML一共要请求10个资源,那么请求的总时间是每一个资源请求时间的总和。最直观的体验就是,网速越快请求时间越短。然而如果某一个资源的请求被阻塞了(比如 SQL 语句执行非常慢)。但对于客户端来说所有后续的请求都会因此而被阻塞。这就是队头拥塞,如下图,如果第四个资源的传输花了很久,后面的资源都得等着,平白浪费了很多时间,带宽资源没有得到充分利用。
所以起初解决问题的方法是,开启并发请求:
可见虽然第四个资源的请求被阻塞了,但是其他的资源请求并不一定会被阻塞,这样总的来说网络的平均利用率得到了提升。有读者可能会好奇,为什么不多搞几个并发的 HTTP 请求呢?刚刚说过笔者的电脑最多支持 6 个并发请求,谷歌曾经做过实验,把 6 改成 10,然后尝试访问了三千多个网页,发现平均访问时间竟然还增加了 5% 左右。这是因为一次请求涉及的域名有限,再多的并发 HTTP 请求并不能显著提高带宽利用率,反而会消耗性能。
支持并发请求是解决队头拥塞的方案之一,但是队头拥塞的问题依然存在,只是不浪费带宽了而已,如果一条队列拥塞,还是对资源又不小的浪费。
2012年google如一声惊雷提出了SPDY的方案。SPDY协议的做法很值得借鉴,它采用了多路复用(Multiplexing) 技术,允许多个HTTP请求共享同一个TCP连接。我们假设每个资源被分为多个包传递,在HTTP1.1中只有前面一个资源的所有数据包传输完毕后后面资源的包才能开始传递(HOC问题),而SPDY并不这么要求,大家可以一起传输。
这么做的代价是数据会略微有一些冗余,每一个资源的数据包都要带上标记,用来指明自己属于哪个资源,这样客户端最后才能把他们正确的拼接起来。不同的标记可以理解为图中不同的颜色,每一个小方格可以理解为资源的某一个包。
- 降低延迟。针对HTTP高延迟的问题,SPDY优雅的采取了多路复用(multiplexing)。多路复用通过多个请求stream共享一个tcp连接的方式,解决了HOL blocking的问题,降低了延迟同时提高了带宽的利用率。
- 请求优先级(request prioritization)。多路复用带来一个新的问题是,在连接共享的基础之上有可能会导致关键请求被阻塞。SPDY允许给每个request设置优先级,这样重要的请求就会优先得到响应。比如浏览器加载首页,首页的html内容应该优先展示,之后才是各种静态资源文件,脚本文件等加载,这样可以保证用户能第一时间看到网页内容。
- header压缩。前面提到HTTP1.x的header很多时候都是重复多余的。选择合适的压缩算法可以减小包的大小和数量。
- 安全性。基于HTTPS的加密协议传输,大大提高了传输数据的可靠性。
- 服务端推送。采用了SPDY的网页,例如我的网页有一个sytle.css的请求,在客户端收到sytle.css数据的同时,服务端会将sytle.js的文件推送给客户端,当客户端再次尝试获取sytle.js时就可以直接从缓存中获取到,不用再发请求了。
SPDY位于HTTP之下,TCP和SSL之上,这样可以轻松兼容老版本的HTTP协议(将HTTP1.x的内容封装成一种新的frame格式),同时可以使用已有的SSL功能。
HTTP2.0
顾名思义有了HTTP1.x,那么HTTP2.0也就顺理成章的出现了。
HTTP2.0可以说是SPDY的升级版(其实原本也是基于SPDY设计的),但是,HTTP2.0 跟 SPDY 仍有不同的地方,2.0主要特性:
- 二进制格式。HTTP1.x的解析是基于文本。基于文本协议的格式解析存在天然缺陷,文本的表现形式有多样性,要做到健壮性考虑的场景必然很多,二进制则不同,只认0和1的组合。基于这种考虑HTTP2.0的协议解析决定采用二进制格式,实现方便且健壮。
-
多路复用。即连接共享,即每一个request都是是用作连接共享机制的。一个request对应一个id,这样一个连接上可以有多个request,每个连接的request可以随机的混杂在一起,接收方可以根据request的id将request再归属到各自不同的服务端请求里面。如SPDY相同。
- header压缩。如上文中所言,对前面提到过HTTP1.x的header带有大量信息,而且每次都要重复发送,HTTP2.0使用encoder来减少需要传输的header大小,通讯双方各自cache一份header fields表,既避免了重复header的传输,又减小了需要传输的大小。
- 服务端推送。
网络协议的优化
其实从1.0到1.1到HTTPS再到SPDY和2.0,我们可以看到这就是一个优化的过程,那么还有哪些方案呢?
滑动窗口
是不是觉得 SPDY 的多路复用已经够厉害了,解决了队头阻塞问题?很遗憾的是,并没有,而且我可以很肯定的说,只要你还在用 TCP 链接,HOC 就是逃不掉的噩梦,不信我们来看看 TCP 的实现细节。
我们知道 TCP 协议会保证数据的可达性,如果发生了丢包或者错包,数据就会被重传。于是问题来了,如果一个包丢了,那么后面的包就得停下来等这个包重新传输,也就是发生了队头阻塞。当然 TCP 协议的设计者们也不傻,他们发明了滑动窗口的概念。
这样的好处是在第一个数据包(1-1000) 发出后,不必等到 ACK 返回就可以立刻发送第二个数据包。可以看出图中的 TCP 窗口大小是 4,所以第四个包发送后就会开始等待,直到第一个包的 ACK 返回。这样窗口可以向后滑动一位,第五个包被发送。
如果第一、二、三个的包都丢失了也没有关系,当发送方收到第四个包时,它可以确信一定是前三个 ACK 丢了而不是数据包丢了,否则不会收到 4001 的 ACK,所以发送方可以大胆的把窗口向后滑动四位。
滑动窗口的概念大幅度提高了 TCP 传输数据时抗干扰的能力,一般丢失一两个 ACK 根本没关系。但如果是发送的包丢失,或者出错,窗口就无法向前滑动,出现了队头阻塞的现象。
运输层-QUIC协议多路复用与纠错
所以说HOC不仅仅在HTTP层存在,在TCP层也存在,这也正是QUIC协议要解决的问题。回顾SPDY是如何解决HOC的,没错,多路复用(Multiplex)。QUIC协议也采用了多路复用技术。
QUIC协议基于UDP实现,我们知道UDP协议只负责发送数据,并不保证数据可达性。SPDY为各个数据包做好标记,指明他们属于哪个HTTP请求,至于这些包能不能到达客户端,SPDY并不关心,因为数据可达性由TCP协议保证。既然客户端一定能收到包,那就只要排序、拼接就行了。QUIC协议采用了多路复用度思想,但同时还得自己保证数据的可达性。
TCP协议的丢包重传并不是一个好想法,因为一旦有了前后顺序,队头阻塞问题将不可避免。而无序的数据发送给接受者以后,如何保证不丢包,不错包呢?这看起来是个不可能完成的任务,不过如果把要求降低成:“最多丢一个包,或者错一个包”,事情就简单多了,操作系统中有一种存储方式叫RAID 5,采用的是异或运算加上数据冗余的方式来保证前向纠错。
我们知道异或运算的规则是,0 ^ 1 = 1
、1 ^ 1 = 0
,也就是相同数字异或成 1,不同数字异或成 0。对两个数字做异或运算,其实就是将他们转成二进制后按位做异或,因此对于任何数字 a,都有:
a ^ a = 0
a ^ 0 = a
同时很容易证明异或运算满足交换律和结合律,我们假设有下面这个等式:
A1 ^ A2 ^ A3 ^ ... ^ An = T
如果想让等式的左边只留下一个一个元素,只要在等号两边做 n-1 次异或就可以了:
(A1 ^ A1) ^ A2 ^ A3 ^ ... ^ An = T ^ A1
// 所以
A2 ^ A3 ^ ... ^ An = T ^ A1
// 所以
A3 ^ ... ^ An = T ^ A1 ^ A2
// 所以 ......
Ai = T ^ A1 ^ A2 ^ ... Ai-1 ^ Ai+1 ^ Ai+2 ^ ... ^ An
换句话说,A1 到 An 和 T 这总共 n+1 个元素中,不管是任何一个元素缺失,都可以从另外 n 个元素推导出来。如果把 A1、A2 一直到 An 想象成要发送的数据,T 想象成冗余数据,那么除了丢包重传,我们还可以采用冗余数据包的形式来保证数据准确性。
举个例子,假设有 5 个数据包要发送,我可以额外发送一个包(上面例子中的 T),它的值是前五个包的异或结果。这样不管是前五个包中丢失了任何一个,或者某个包数据有错(可以当成丢包来处理),都可以用另外四个包和这个冗余的包 T 进行异或运算,从而恢复出来。
当然要注意的是,这种方案仅仅在只发生一个错包或丢包时有效,如果丢失两个包就无能为力了(这也就是为什么只发一个冗余包就够的原因)。因此数据包和冗余包之间的比值需要精心设计,如果比值过高,很容易出现丢两个包的情况,如果比值过低,又会导致冗余度太高,需要设计者根据概率计算结果进行权衡。
利用冗余数据的思想,QUIC 协议基本上避免了重发数据的情况,这种利用已有数据就能进行错误恢复的技术叫做前向恢复。当然 QUIC 协议还是支持重传的,比如某些非常重要的数据或者丢失两个包的情况。
更少的RTT
我们考虑一次 HTTPS 请求,它的基本流程是三次 TCP 握手外加四次 SSL/TLS 握手,从图中可以看到这需要三个 RTT:
对于 HTTP 2.0 来说,本来需要一个额外的 RTT 来进行协商,判断客户端与服务器是不是都支持 HTTP 2.0,不过好在它可以和 SSL握手的请求合并。这也就是为什么大多数主流浏览器(比如 Chrome、Firefox) 仅支持 HTTPS 2.0 而不单独支持 HTTP 2.0 的原因,毕竟 HTTP 2.0 需要一个额外的 RTT,HTTPS 2.0 需要两个额外的 RTT,仅仅是增加一个 RTT就能获得数据安全性,还是很划算的。
SSL 握手优化
由于需要确认证书,生成多个随机数来保证安全,握手阶段的两个 RTT 很难节省。不过之前我们见过 HTTP 的 Pipeline 技术可以复用 TCP 连接,那么按照类似的思想,SSL 连接也可以被恢复。思考一下为什么 SSL 要设计这么复杂的握手机制,它本质上是为了保证对称秘钥的安全传输,所以 SSL 会话恢复主要考虑的也是如何恢复对称秘钥。
一个常用的方案是采用Session Ticket,实现起来很容易: 一旦 SSL 会话建立起来,服务端把会话的基本信息,比如对称秘钥、加密方法等信息加密后发给客户端,客户端可以缓存下来这个 Session Ticket。需要恢复 SSL 会话时直接把它发回给服务端校验即可,这样可以在 SSL 层减少一个 RTT。
TCP快速打开
聊完了SSL层,下面说说TCP的优化方案。我们都知道TCP的三次握手需要花费一个 RTT,有没有可能做到0-RTT呢?比如我们在握手的时候就带上要传递的数据。
实际上 TCP 协议已经规定了这种情况的处理方式,即客户端可以在发送第一个 SYN 握手包时携带数据,但是 TCP 协议的实现者绝对不允许把这个数据包上传给应用层,这主要是为了防止TCP泛洪攻击。
TCP 泛洪攻击是指攻击者利用多台机器发送SYN请求从而耗尽服务器的backlog队列(backlog队列维护的是那些接受了SYN请求但还没有正式开始会话的连接)。这样做的好处是服务器不会过早的分配端口、建立连接。RFC 4987详细的描述了各种防止 TCP 泛洪攻击的方法,包括尽早释放 SYN,增加队列长度等等。
如果SYN握手的包能被传输到应用层,那么现有的防护措施都无法防御泛洪攻击,而且服务端也会因为这些攻击而耗尽内存和CPU。所以人们设计了TFO (TCP Fast Open),这是对 TCP 的拓展,不仅可以在发送 SYN 时携带数据,还可以保证安全性。
TFO设计了一个cookie,它在第一次握手时由server生成,cookie主要是用来标识客户端的身份,以及保存上次会话的配置信息。因此在后续重新建立TCP连接时,客户端会携带 SYN + Cookie + 请求数据,然后不等 ACK 返回就直接开始发送数据。
服务端收到 SYN 后会验证 cookie 是否有效,如果无效则会退回到三次握手的步骤,如下图所示:
同时,为了安全起见,服务端为每个端口记录了一个值 PendingFastOpenRequests
,用来表示有多少请求利用了 TFO,如果超过预设上限就不再接受。
关于 TFO 的优化,可以总结出三点内容:
- TFO 设计的 cookie 思想和 SSL 恢复握手时的 Session Ticket 很像,都是由服务端生成一段 cookie 交给客户端保存,从而避免后续的握手,有利于快速恢复。
- 第一次请求绝对不会触发 TFO,因为服务器会在接收到 SYN 请求后把 cookie 和 ACK 一起返回。后续客户端如果要重新连接,才有可能使用这个 cookie 进行 TFO
- TFO 并不考虑在 TCP 层过滤重复请求,以前也有类似的提案想要做过滤,但因为无法保证安全性而被拒绝。所以 TFO 仅仅是避免了泛洪攻击(类似于 backlog),但客户端接收到的,和 SYN 包一起发来的数据,依然有可能重复。不过也只有可能是 SYN 数据重复,所以 TFO 并不处理这种情况,要求服务端程序自行解决。这也就是说,不仅仅要操作系统的支持,更要求应用程序(比如 MySQL) 也支持 TFO。
0-RTT
TFO使得TCP协议有可能变成 0-RTT,核心思想和Session Ticket 的概念类似::将当前会话的上下文缓存在客户端。如果以后需要恢复对话,只需要将缓存发给服务器校验,而不必花费一个 RTT 去等待。
结合 TFO 和 Session Ticket 技术,一个本来需要花费 3 个 RTT 才能完成的请求可以被优化到一个 RTT。如果使用 QUIC 协议,我们甚至可以更进一步,将 Session Ticket 也放到 TFO 中一起发送,这样就实现了 0-RTT 的对话恢复。感兴趣的读者可以阅读: Facebook App对TLS的魔改造:实现0-RTT
Why QUIC
从以上分析可以发现,HTTP2.0 和 SSL 可以说已经进行了大量的优化,可以提升的空间非常小。而 TCP 存在诸多不足之处,一方面它设计较早,而且主要目的是设计一种通用、可靠的传输协议,并非专门为网页或者 App 而设计,另一方面对 TCP 的改进要比对 SSL 和 HTTP 的改进麻烦的多,因为 TCP 是由各个操作系统实现,就以 TFO 为例吧,它在新版本的 Linux 内核中被实现,但想等到它普及开来就不知道要到猴年马月了,有兴趣的读者可以参考参考现在 Windows XP 系统的市场占有率。
反观 HTTP 和 SSL,虽然早期 HTTP 1.0 的问题更多,但是经过 1.1、SPDY、2.0 等版本的更迭,已经非常优秀了。其中的根本原因还是在于 HTTP 和 SSL 位于应用层,优化升级比较容易实现,所以经过长年累月的优化升级,现在大部分瓶颈都集中于 TCP 层。但 TCP 不仅优化点较多,而且还不容易更新。那么能不能在传输层搞一个和 TCP、UDP 类似的协议呢?答案也是否定的,其实曾经有一个 SCTP 协议打算进行一系列优化,但并没有被广泛接受。这是因为数据在传输的过程中需要经过各个路由器,这些设备只能识别并解析 TCP 和 UDP 协议的数据包,无法解析新的协议。所以 SCTP 也只能用于内网的实验环境中。
TCP 要改进,但不方便改,新增一个协议又不被已有的设备支持,看起来唯一的方案就是使用 UDP 了。虽然 UDP 协议不保证数据可达性,但这也是 UDP 的优点所在,它天然支持 0-RTT 的通信,所以一个比较新颖激进的想法就冒出来了:
采用 UDP 作为底层协议,在 UDP 之上实现数据可达性
目前,QUIC 协议内置在 Chrome 浏览器中,每次更新只需要升级浏览器即可,在 2014 年前就已经迭代了 13 个版本。
总结
- QUIC 协议有哪些优点,如何实现 0-RTT?
- QUIC 协议在传输层就支持多路复用,避免了队头阻塞问题。
- QUIC 协议基于 UDP,更自由更高效
- QUIC 协议借鉴了 TFO 的思想,支持会话上下文缓存,方便恢复,具备实现 0-RTT 的可能
- 传统的 HTTP2 + SSL + TCP 协议栈有哪些缺点?
- SSL 的会话恢复依然需要一个 RTT,而且难以合并到 TCP 层
- TCP 的滑动窗口存在队头阻塞问题
- TCP 的重传纠错会浪费一个 RTT
- 为什么 Google 要另起炉灶,基于 UDP 去做?
- TCP 由操作系统实现,很难更新
- UDP 非常高效,几乎没有性能负担
- 将 QUIC 嵌入到 Chrome 中可以方便后续的升级迭代