首次提出概念
HTTP 是浏览器与服务端之间最主要的通信协议。
20世纪60年代,美国国防部高等研究计划署(ARFA)建立了APFA网,这被认为是互联网的起源。
70年代,研究人员基于对ARPA网的实践与思考,发明出了著名的TCP/IP协议。该协议有良好的分层结构和稳定的性能,并在 80 年代中期进入了 UNIX 系统内核,促使更多的计算机接入了网络。
1989 年,蒂姆伯纳斯-李博士发表了一篇论文,提出了在互联网上构建超链接文档系统的构想。在篇文章中他确立了三项关键技术:URI
、HTML
、HTTP
。
基于这三项技术,可以把超文本系统完美地运行在互联网上,李博士把这个系统称为“万维网”(World Wide Web
)。
传递 HTML的HTTP/0.9
1991 年 HTTP/0.9(HyperText Transfer Protocol
)正式诞生,主要用于网络之间传递 HTML,所以被称为超文本传输协议
。
协议定义了客户端发起请求、服务端响应请求的通信模式。
请求报文内容只有 1 行:GET + 请求的文件路径
如:
GET /top index.html // 用来获取 index.html
特点:
- 只有一个请求行就可以完整表达客户端需求,所以
没有HTTP请求头以及请求体和响应体
。 - 由于服务器只需要返回数据,无需告诉浏览器其他信息,所以
没有返回响应头信息
。 - 因为返回数据是HTML格式,所以返回的文件内容
以ASCII字符流传输
最为合适。
HTTP/0.9 虽然简单,但是它充分验证了 Web 服务的可行性:把简单的系统变复杂,要比把复杂的系统变简单容易得多。
被浏览器推动的http1.0
随着互联网的发展以及浏览器的出现,单纯的文本内容已经无法满足用户需求了,浏览器还希望通过 HTTP 来传输脚本、样式、图片、音频和视频等不同类型的文件,因而,支持多类型文件下载
是HTTP/1.0的核心诉求,因为文件的格式不能仅仅局限于ASCII编码,还有很多类型编码的文件。
那如何实现多种类型文件的下载呢?
首先,我们得先找到解决问题的方向:
- 浏览器得告诉服务器它需要什么
语言版本
的界面; - 浏览器要知道服务器
返回的数据是什么类型
才能根据不同类型的数据进行针对性处理。 - 由于万维网所支持的应用变得越来越广,所以单个文件的数据量也越来越大,为了减轻传输性能,服务器会对数据进行压缩后再传输,所以服务器要知道浏览器要知道
服务器压缩数据的方式
。 - 各种文件的编码形式可能不一样,浏览器需要知道
文件的编码类型
。
🚀基于以上问题,1996 年HTTP/1.0引入以key-value
形式保存的请求头和响应头,在HTTP发送请求时,会带上请求头信息,服务器返回数据时,会返回响应头信息。
最终发出的请求头内容如下:
accept:text/html // 请求的文件类型
accept-encodeing: gzip,deflate // 服务器采用的压缩方式
accept-charset: utf-8 // 文件的编码格式
accept-language:zh-CN,zh // 页面显示的语言
服务器接收到浏览器发送过来的请求头信息以后,会根据请求头信息来响应数据。之后浏览器就根据响应头信息,处理数据。比如:
content-encoding :gzip // 服务器响应数据的压缩格式
content-type:text/html;charset=UTF-8 // 服务器响应的 数据类型 和 编码格式
有了这个响应头,浏览器会使用gzip
方式来解压文件,再按照UTF-8
的编码格式来处理原始文件,最后使用HTML
的方式来解析文件。
🚀这就是 HTTP/1.0支持多文件的一个基本流程。
HTTP/1.0除了对多文件提供良好的支持,还依据当时的实际需求引入了很多其他特性,且都是通过请求头和响应头来实现的。比如:
引入状态码
:有的请求服务器可能无法处理或者处理出错,这时候就需要告诉浏览器服务器最终处理该请求的情况,这就引入了状态码,状态码通过响应行的方式来通知浏览器。提供Cache机制
:缓存已经下载过的数据,以此来减轻服务器的压力。加入了用户代理字段
:服务器需要统计客户端的基础信息,比如windows1
和macOS
的用户数量分别是多少,所以HTTP/1.0的请求头还加入了用户代理字段。
除此之外,增加了 HEAD、POST 等新方法,引入了协议版本号概念。
但是 HTTP/1.0 并不是一个“标准”,只是一份参考文档,不具有实际的约束力。
根据需求迭代的HTTP/1.1
渐渐的,HTTP/1.0也无法满足需求,最核心的就是连接问题。所以,HTTP/1.1出来了!!
改进持久连接
HTTP/1.0每进行一次通信,都要经历一次建立连接
、传输数据
和断开连接
三个阶段。当一个页面引用了较多的外部文件时,这个建立连接和断开连接的过程就会增加大量网络开销。
为了解决这个问题,HTTP/1.1中增加了持久化连接的方法,允许一个TCP连接上传输多个HTTP请求,只要浏览器或服务器没有明确断开连接,那么该TCP会一直保持。
HTTP的持久化连接可以有效减少TCP连接的次数,可以减少服务器额外的负担,并提升整体HTTP的请求时长。
持久化连接在HTTP/1.1中是默认开启的(
Connection: keep-alive
),如果不需要持久连接,也可以在HTTP请求头加上Connection:close
。
目前谷歌浏览器对于同一个域名,默认允许同时建立6个TCP持久连接。
提供虚拟主机的支持
在HTTP/1.0中,每一个域名单独绑定一个唯一的IP地址,因此一个服务器只能支持一个域名,但是随着虚拟主机技术的发展,需要在一个物理机上绑定多个虚拟机,每个虚拟机都有自己单独的域名,这些单独的域名都共用一个IP地址。
因此,HTTP/1.1的请求头中增加了Host字段
,用来表示当前域名地址,这样服务器就能根据不同的Host地址做不同的处理。
不成熟的HTTP管线化
持久连接虽然能减少TCP的建立和断开次数,但是它需要等待前面的请求返回之后,才能进行下一次请求。如果TCP通道中的某个请求因为某些原因没有及时返回,那么就会阻塞后面的所有请求,这就是著名的队头阻塞的问题。
HTTP.1/1试图通过管线化
的技术来解决队头阻塞的问题。
HTTP/1.1的管线化是指将多个HTTP请求整批提交给服务器的技术。
虽然可以整批发送请求,不过服务器依然需要根据请求顺序来回复浏览器的请求。FireFox、Chrome都做过管线化的试验,但是由于各种原因,最终都放弃了管线化技术。
对动态生成的内容提供了完美的支持
在设计HTTP/1.0时,需要在响应头设置完整的数据大小,如Content-Length:901
,这样浏览器就可以根据设置的数据大小来接收数据。不过随着服务器端的技术发展,很多页面的内容都是动态生成的,因此在传输数据之前并不知道最终的数据大小,这就导致了浏览器不知道何时会接收完所有的文件数据。
HTTP/1.1通过引入Chunk transfer
机制来解决这个问题,即服务器会将数据分割成若干个任意大小的数据块,每个数据块发送时会附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志,这样就提供了对动态内容的支持。
客户端Cookie、安全机制
http的无状态性简化了服务器的设计,不需要保存状态,使服务器更容易支持大量并发请求;但是对于一些需要登入认证的网页来说,每次跳转新页面不是要再次登入,就是要在每次的请求报文中附加参数来管理登入状态,这样就很麻烦,效率很低。
http1.1引入cookie技术:cookie是客户端存储状态的机制,客户端第一次访问某需要认证的网站时,填入认证信息后,向服务器发送请求报文,服务器收到报文后,可以在其相应报文的首部字段Set-Cookie
中加入cookie,cookie存着服务器给客户端的特殊信息(用户名、密码、过期时间、路径和域
),客户端收到响应报文后会保存这个cookie,当客户端再次发送请求报文的时候,就会自动带上cookie(即使不需要)
,服务器收到此cookie,就知道这个是“老用户”了。
其实HTTP 1.1通过增加更多的请求头和响应头来改进和扩充HTTP 1.0的功能。
比如HTTP 1.1还提供了与身份认证、状态管理和Cache缓存等机制相关的请求头和响应头。HTTP/1.0不支持文件断点续传,
RANGE:bytes
是HTTP/1.1新增内容,HTTP/1.0每次传送文件都是从文件头开始,即0字节处开始。RANGE:bytes=XXXX
表示要求服务器从文件XXXX字节处开始传送,这就是我们平时所说的断点续传。
总结
:
HTTP/1.1具有以下特点:
长连接(Connection:keep-live)
:引入了TCP连接复用,即一个TCP默认不关闭,可以被多个请求复用并发连接
:对一个域名的请求允许分配多个长连接(缓解了长连接中的对头阻塞问题)引入管道机制
:一个TCP连接,可以同时发送多个请求。(响应的顺序必须跟请求的顺序一致,因此不常用)- 增加了PUT、DETELE、OPTIONS、PATCH等新的方法
带宽优化及网络连接的使用
:HTTP1.0中,存在一些浪费带宽的现象,例如客户端只是需要某个对象的一部分,而服务器却将整个对象送过来了,并且不支持断点续传功能,HTTP1.1则在请求头引入了range
头域,它允许只请求资源的某个部分,即返回码是206(Partial Content)
,这样就方便了开发者自由的选择以便于充分利用带宽和连接。运行响应数据分块(chunked)
:利于传输大文件强制要求host头
:让互联网主机托管成为可能。错误通知的管理
:在HTTP1.1中新增了24
个错误状态响应码,如409(Conflict)表示请求的资源与资源的当前状态发生冲突;410(Gone)表示服务器上的某个资源被永久性的删除。缓存处理
,在HTTP1.0中主要使用header
里的If-Modified-Since
,Expires
来做为缓存判断的标准,HTTP1.1则引入了更多的缓存控制策略例如Entity tag
,If-Unmodified-Since
,If-Match
,If-None-Match
等更多可供选择的缓存头来控制缓存策略。
HTTP/1.1 与 HTTP/1.0 的一个重要区别是:HTTP/1.1 是一个“正式的标准”
此后互联网上所有的浏览器、服务器、网关、代理等,只要用到 HTTP 协议,就必须严格遵守这个标准。
解决问题的HTTP2
HTTP2.0是HTTP协议自1999年HTTP1.1发布后的首个更新,主要基于SPDY
协议。HTTP2.0大幅度的提高了web性能,在HTTP1.1完全语义兼容的基础上,进一步减少了网络的延迟。实现低延迟高吞吐量。
SPDY是Speedy的昵音,意为“更快”。它是Google开发的基于TCP协议的应用层协议。目标是优化HTTP协议的性能,通过压缩、多路复用和优先级等技术,缩短网页的加载时间并提高安全性。SPDY协议的核心思想是尽量减少TCP连接数。SPDY并不是一种用于替代HTTP的协议,而是对HTTP协议的增强。
HTTP1.1的改进和缺点
改进
我们知道HTTP/1.1为网络效率做了大量的优化
- 增加了持久连接
- 浏览器为每个域名最多同时维护6个TCP持久连接
- 使用CND实现域名分片机制
如图所示,引入CDN的同时为每个域名建立了6个连接,这样就大大减轻了整个资源的下载时间。
例:一个TCP的持久连接,下载100个资源所花费的时间为100×n×RTT
,优化后花费的时间为100×n×RTT/(6×CDN个数)
存在问题
对带宽的利用不理想,之所以说HTTP/1.1对带宽的利用率不理想,比如我们装的100M带宽,实际下载速度能达到12.5M/S,而采用HTTP/1.1时,也许再加载页面资源时最大只能使用 2.5M/s,很难讲12.5M全部用满。
带宽是指每秒能够最大发送或者接收的字节数,每秒能够发送的最大字节数称之为上行带宽,每秒能够接收的最大字节数称之为下行带宽。
之所以出现这个问题主要是以下三个原因导致的,先总结一波~
慢启动和TCP连接之间相互竞争带宽是由于TCP本身的机制导致的,而队头阻塞是由于HTTP/1.1的机制导致的。
具体请往下看:
TCP的慢启动
TCP建立连接之后,就进入到发送数据状态,一开始TCP会采用一个非常慢的速度去发送数据,然后慢慢加快发送数据的速度,直到发送数据的速度达到一个理想状态。这个过程叫做慢启动。慢启动是TCP为了减少网络拥堵的一种策略,我们是没有办法改变的。
之所以说慢启动会带来性能问题,是页面中的一下关键资源本来就不大,如HTML、CSS、JavaScript文件,通常将这些文件在TCP连接建立好以后就要发起请求的,但是这个过程是慢启动,所以耗费的时间比正常时间多很多,这样就推迟了宝贵的首次渲染的时间。
同时开启多条TCP连接,那么这些连接会竞争固定的带宽
系统同时建立了多条TCP连接,当带宽充足时,每条连接发送或者接收速度会慢慢向上增加;而一旦带宽不足时,这些TCP连接又会减慢发送或者接收的速度。
这样就会出现一个问题
,因为有的TCP连接下载的是一些关键资源,如CSS文件、JavaScript文件等,而有的TCP连接下载的是图片、视频等普通的资源文件,但是多条TCP连接之间又不能协商让哪些关键资源优先下载,这样就有可能影响那些关键资源的下载速度了。
HTTP/1.1队头阻塞的问题
在HTTP/1.1中使用持久连接时,虽然能公用一个TCP管道,但是在一个管道中同一时刻只能处理一个请求,在当前的请求没有结束之前,其他的请求只能处于阻塞状态。
这意味着我们不能随意在一个管道中发送请求和接收内容。
这是一个很严重的问题,因为阻塞请求的因素有很多,并且都是一些不确定性的因素,假如有的请求被阻塞 了5秒,那么后续排队的请求都要延迟等待5秒,在这个等待的过程中,带宽、CPU都被白白浪费了。
在浏览器处理生成页面的过程中,是非常希望能提前接收到数据的,这样就可以对这些数据做预处理操作,比如提前接收到了图片,那么就可以提前进行编解码操作,等到需要使用该图片的时候,就可以直接给出处理后的数据了,这样能让用戶感受到整体速度的提升。
但队头阻塞使得这些数据不能并行请求,所以队头阻塞是很不利于浏览器优化的。
于是,HTTP/2来了!
HTTP/2改进(多路复用)
慢启动和TCP连接之间相互竞争带宽是由于TCP本身的机制导致的,但是我们没有换掉TCP的能力,所以我们只能想办法规避TCP的慢启动和TCP连接之间的竞争问题。
基于此,2015年发布的HTTP/2的解决方案可以总结为:一个域名只使用一个TCP连接和消除队头阻塞问题
。
一个域名只使用一个TCP⻓连接来传输数据,这样整个页面资源的下载过程只 需要一次慢启动,同时也避免了多个TCP连接竞争带宽所带来的问题。 另外,就是队头阻塞的问题,等待请求完成后才能去请求下一个资源,这种方式无疑是最慢的,所以 HTTP/2需要实现资源的并行请求,也就是任何时候都可以将请求发送给服务器,而并不需要等待其他请求的完成,然后服务器也可以随时返回处理好的请求资源给浏览器。
看图:HTTP/2最核心、最重要且最具颠覆性的多路复用机制。
从图2中你会发现每个请求都有一个对应的 ID,如stream1
表示index.html
的请求,stream2
表示foo.css
的请求。这样在浏览器端,就可以随时将请求发送给服务器了。
服务器端接收到这些请求后,会根据自己的喜好来决定优先返回哪些内容,比如服务器可能早就缓存好index.html和bar.js的响应头信息,那么当接收到请求的时候就可以立即把index.html和bar.js的响应头信息返回给浏览器,然后再将index.html和bar.js的响应体数据返回给浏览器。
为啥现在就可以随意发送了呢?
之所以可以随意发送,是因为每份数据都有对应的ID,浏览器接收到之后,会筛选出相同ID的内容,将其拼接为完整的HTTP响应数据。
而且HTTP/2使用了多路复用技术,可以将请求分成一帧一帧的数据去传输,这样带来了一个额外的好处,就是当收到一个优先级高
的请求时,比如接收到JavaScript或者CSS关键资源的请求,服务器可以暂停之前的请求来优先处理关键资源的请求。
实现多路复用
HTTP/2添加了一个二进制分帧层
来实现多路复用,有了二进制分帧后,对于同一个域,客户端只需要与服务端建立一个连接即可完成通信需求,这种利用一个连接来发送多个请求的方式称为多路复用。每一条路都被称为一个 stream(流)。
HTTP/2 中的二进制帧是如何设计的?
帧结构
HTTP/2 中传输的帧结构如下图所示:
每个帧=帧头+帧体
在帧头中,先是三个字节的帧长度,表示帧体的长度,然后是帧类型,大概可以分为数据帧和控制帧两种,数据帧用来放HTTP报文,控制帧用来管理流的传输。
接下来的一个字节是帧标识,里面一共有8个控制位,常用的有END_HENDERS
表示头数据结束,END_STREAM
表示单方向数据发送结束。
后 4 个字节是Stream ID
, 也就是流标识符
,有了它,接收方就能从乱序的二进制帧中选择出 ID 相同的帧,按顺序组装成请求/响应报文。
在 HTTP/2 中,所谓的
流
,其实就是二进制帧的双向传输的序列
。那么在 HTTP/2 请求和响应的过程中,流的状态是如何变化的呢?
流的状态变化
HTTP/2 其实也是借鉴了 TCP 状态变化的思想,根据帧的标志位来实现具体的状态改变。这里我们以一个普通的请求-响应
过程为例来说明:
最开始两者都是空闲状态,当客户端发送Headers
帧后,开始分配Stream ID
,此时客户端的流打开,服务端接受之后服务端的流也打开,两端的流都打开后,就可以互相传数据帧和控制帧了。
当客户端要关闭时,向服务端发送END_STREAM
帧,进入半关闭状态,这个时候客户端只能接受数据,而不能发送数据。
服务端收到这个END_STREAM
帧后也进入半关闭状态
,不过此时服务端的情况是只能发送数据,而不能接收数据。随后服务端也向客户端发送END_STREAM
帧,表示数据发送完毕,双方进入关闭状态
。
如果下次要开启新的
流
,流 ID 需要自增,直到上限为止,到达上限后开一个新的 TCP 连接从头开始计数。由于流 ID 字段长度为 4 个字节,最高位又被保留,因此范围是 0 ~ 2的 31 次方,大约 21 亿个。
流的特性
刚谈到了流的状态变化过程,这里顺便就来总结一下流
传输的特性:
-
并发性。一个 HTTP/2 连接上可以同时发多个帧,这一点和 HTTP/1 不同。这也是实现多路复用的基础。
-
自增性。流 ID 是不可重用的,而是会按顺序递增,达到上限之后又新开 TCP 连接从头开始。
-
双向性。客户端和服务端都可以创建流,互不干扰,双方都可以作为
发送方
或者接收方
。 -
可设置优先级。可以设置数据帧的优先级,让服务端先处理重要资源,优化用户体验
分析完了之后,那我们就结合图来分析下HTTP/2的请求和接收过程。
如图,HTTP/2添加了一个二进制分帧层
。
- 在浏览器准备好请求数据(包括请求行、请求头等信息,如果是post就再加上请求体)
- 这些数据在经过二进制分帧层处理之后,会被转换成一个个带有请求ID编号的帧,通过这些协议栈将这些帧发送给服务器
- 服务器接收到所有帧之后,会将所有相同的帧合并为一条完整的请求信息
- 然后服务器处理该条请求,并将处理的响应行、响应头和响应体分别发送至二进制分帧层。
- 同样,二进制分帧层会将这些响应数据转换成一个个带有请求ID编号的帧,经过协议栈发送给浏览器
- 浏览器接收到响应帧后,会根据ID编号将帧的数据提交给对应的请求
至此,HTTP的多路复用技术就讲完了。
注意:
HTTP是浏览器和服务器通信的语言,在这里虽然HTTP/2引入了二进制分帧层,不过HTTP/2的语义和HTTP/1.1依然是一样的,也就是说它们通信的语言并没有改变,比如开发者依然可以通过Accept
请求头告诉服务器希望接收到什么类型的文件,依然可以使用Cookie来保持登录状态,依然可以使用Cache
来缓存本地文件,这些都没有变,发生改变的只是传输方式。这一点对开发者来说尤为重要,这意味着我们不需要为HTTP/2去重建生态,并且HTTP/2推广起来会也相对更轻松了。
HTTP/2其他特性
通过设置数据帧的优先级,让服务器优先处理某些请求
浏览器中共有些数据是很重要的,但是在发送请求时,重要的请求可能是晚于那些不怎么重要的请求,如果服务器按照请求的数据来回复数据,那么这个重要的数据就有可能推迟很久才能到达浏览器,这对于用户体验来说很不好。
为了解决这个问题,HTTP/2提供了请求优先级,可以在发送请求时,标上该请求的优先级,这样服务器接 收到请求之后,会优先处理优先级高的请求。
允许服务器主动向客户推送数据
HTTP/2还可以直接将数据提前推送到浏览器。你可以想象这样一个场景,当用 戶请求一个HTML页面之后,服务器知道该HTML页面会引用几个重要的JavaScript文件和CSS文件,那么在 接收到HTML请求之后,附带将要使用的CSS文件和JavaScript文件一并发送给浏览器,这样当浏览器解析完HTML文件之后,就能直接拿到需要的CSS文件和JavaScript文件,这对首次打开页面的速度起到了至关重要的作用。
头部压缩
在 HTTP/1.1 及之前的时代,
请求体
一般会有响应的压缩编码过程,通过Content-Encoding
头部字段来指定,但你有没有想过头部字段本身的压缩呢?
当请求字段非常复杂的时候,尤其对于 GET 请求,请求报文几乎全是请求头,通常情况下⻚面也有 100个左右的资源,如果将这100个请求头的数据压缩为原来的20%,那么传输效率肯定能得到大幅提升。
HTTP/2 针对头部字段,也采用了对应的压缩算法——HPACK
,对请求头进行压缩。
HPACK 算法是专门为 HTTP/2 服务的,它主要的亮点有两个:
- 首先在服务器与客户端之间
建立哈希表
,将用到的字段存放在这张表中,那么在传输的时候对于之前出现过的值,只要把索引(比如0,1,2……)传给对方即可,对方拿到索引查表
就可以了。这种传索引的方式,让请求头字段得到极大程度的精简与复用。
HTTP/2 当中废除了起始行的概念,将起始行中的请求方法、URI、状态码转换成了头字段,不过这些字段都有一个":"前缀,用来和其它请求头区分开。
- 其次是
对于整数和字符串进行哈夫曼编码
,哈夫曼编码的原理就是先将所有出现的字符建立一张索引表,然后让出现次数多的字符对应的索引尽可能短
,传输的时候也是传输这样的索引序列,可以达到非常高的压缩率。
强化安全
处于兼容的考虑,HTTP/2延续了HTTP/1的“明文”特点,可以像以前一样使用明文传输数据,不强制使用加密通信,不过格式还是二进制,只是不需要解密。
但由于HTTPS已经是大势所趋,而且主流浏览器chrome、Firefox等都公开宣布只支持加密的HTTP/2
,所以,“事实上”的HTTP/2都是加密的。也就是说,互联网上通常所能见到的HTTP/2都是使用“https”协议名,跑在TLS上面。
为了区分“加密”和“明文”这两个不同的版本,HTTP/2协议定义了两个字符串标识,h2
表示加密的HTTP/2,h2c
表示明文的HTTP/2,多出的那个字母c
表示clear text
.
在HTTP/2标准制定的时候(2015年)已经发现了很多SSL/TLS的弱点,而新的TLS1.3
还没有发布,所以加密版本的HTTP/2在安全方面做了强化,要求下层的通信协议必须是TLS1.2
以上,还要支持前向安全和SNI
,并且把一些弱密码套件列入了“黑名单”,比如DES
、RC4
、CBC
、SHA-1
都不能在HTTP/2里面使用,相当于底层用的是“TLS1.25
”
总结一波~
HTTP/2.0 的主要改动包括:
- 数据通过二进制协议传输,不再是纯文本
- 多路复用,废弃了 1.1 中的管道
- 使用专用算法压缩头部,减少数据传输量
- 通过设置数据帧的优先级,让服务器优先处理某些请求
- 允许服务器主动向客户推送数据
- 头部字段全部改为小写;引入了伪头部的概念,出现在头部字段之前,以冒号开头
- 增强了安全性,“事实上”要求加密通信
HTTP/2.0 虽然已经发布了 6 年,不过由于 HTTP/1.1 实在太过经典和强势,目前 HTTP/2.0 的普及率还比较低,仍然有很多网站使用的是 HTTP/1.1 版本,不过国内外一些排名靠前的站点基本都实现了HTTP/2的部署。使用HTTP/2能带来20%〜60%的效率提升,至于20%还是60%要看优化的程度。总之,我们也应该与时俱进,放弃HTTP/1.1和其性能优化方法,去“拥抱”HTTP/2。
走投无路的HTTP/3
HTTP/2确实还是有缺陷!
HTTP/2的缺陷
TCP的队头阻塞
虽然HTTP/2解决了应用层面的队头阻塞问题,不过和HTTP/1.1一样,HTTP/2依然是基于TCP协议的,而 TCP最初就是为了单连接而设计的。你可以把TCP连接看成是两台计算机之间的一个虚拟管道,计算机的一端将要传输的数据按照顺序放入管道,最终数据会以相同的顺序出现在管道的另外一头。
如果在数据传输的过程中,有一个数据因为网络故障或者其他原因而丢包了,那么整个TCP的连接就会处于暂停状态,需要等待丢失的数据包被重新传输过来。
HTTP/2 由于采用二进制分帧
进行多路复用,通常只使用一个 TCP 连接进行传输,在丢包或网络中断的情况下后面的所有数据都被阻塞。这不同于HTTP/1.1,使用HTTP/1.1时,浏览器为每个域名开启了6个TCP连接,如果其中的1个TCP连接发生了队头阻塞,那么其他的5个连接依然可以继续传输数据。
所以随着丢包率的增加,HTTP/2的传输效率也会越来越差。有测试数据表明,当系统达到了2%的丢包率 时,HTTP/1.1的传输效率反而比HTTP/2表现得更好。
TCP建立连接的延时
除了TCP队头阻塞之外,TCP的握手过程
也是影响传输效率的一个重要因素。
网络延迟又称为RTT
(Round Trip Time)。我们把从浏览器发送一个数据包到服务器,再从服务器返回数据包到浏览器的整个往返时间称为RTT(如下图)。RTT
是反映网络性能的一个重要指标。
那建立TCP连接时,需要花费多少个RTT呢?
HTTP/1和HTTP/2都是使用TCP协议来传输的,而如果使用HTTPS的话,还需要使用TLS协议进行 安全传输,而使用TLS也需要一个握手过程,这样就需要有两个握手延迟过程。
在建立TCP连接的时候,需要和服务器进行三次握手来确认连接成功,也就是说需要在消耗完1.5个RTT
之后才能进行数据传输。
进行TLS连接,TLS有两个版本TLS1.2和TLS1.3,每个版本建立连接所花的时间不同,大致是需要1~2个RTT
,关于HTTPS我们到后面到安全模块再做详细介绍。
总之,在传输数据之前,我们需要花掉3〜4个RTT
。
如果浏览器和服务器的物理距离较近,那么1个RTT的 时间可能在10毫秒以内,也就是说总共要消耗掉30〜40毫秒
。这个时间也许用戶还可以接受,但如果服务器相隔较远,那么1个RTT就可能需要100毫秒以上了,这种情况下整个握手过程需要300〜400毫秒
,这时 用戶就能明显地感受到“慢”了。
TCP协议僵化
现在我们知道了TCP协议存在队头阻塞和建立连接延迟等缺点,那我们是不是可以通过改进TCP协议来解决这些问题呢?
答案是:非常困难。之所以这样,主要有两个原因。
第一个是中间设备的僵化。
我们知道互联网是由多个网络互联的网状结构,为了能够保障互联网的正常工作,我们需要在互联网的各处搭建各种设备,这些设备就被称为中间设备
。
这些中间设备有很多种类型,并且每种设备都有自己的目的,这些设备包括了路由器、防火墙、NAT、交换机等。它们通常依赖一些很少升级的软件,这些软件使用了大量的TCP特性,这些功能被设置之后就很少更新了。
所以,如果我们在客戶端升级了TCP协议,但是当新协议的数据包经过这些中间设备时,它们可能不理解包的内容,于是这些数据就会被丢弃掉。这就是中间设备僵化,它是阻碍TCP更新的一大障碍。
除了中间设备僵化外,操作系统也是导致TCP协议僵化的另外一个原因。
因为TCP协议都是通过操作系统内核来实现的,应用程序只能使用不能修改。通常操作系统的更新都滞后于软件的更新,因此要想自由地更新内核中的TCP协议也是非常困难的。
HTTP/3改进
HTTP/2存在一些比较严重的与TCP协议相关的缺陷,但由于TCP协议僵化,我们几乎不可能通过修改TCP协 议自身来解决这些问题,那么解决问题的思路是绕过TCP协议,发明一个TCP和UDP之外的新的传输协议。
但是这也面临着和修改TCP一样的挑战,因为中间设备的僵化,这些设备只认TCP和UDP,如果采用了新的协议,新协议在这些设备同样不被很好地支持。
因此,2018年出的HTTP/3选择了一个折中的方法——UDP协议,基于UDP实现了类似于 TCP的多路数据流、传输可靠性等功能,我们把这套功能称为QUIC协议
。
HTTP/3中的QUIC协议集合了以下几点功能:
1. 实现了类似TCP的流量控制、传输可靠性的功能。
虽然UDP不提供可靠性的传输,但QUIC在UDP的基础之上增加了一层来保证数据可靠性传输,它提供了数据包重传、拥塞控制以及其他一些TCP中存在的特性。
QUIC 协议到底改进在哪些方面呢?主要有如下几点:
- 可插拔 — 应用程序层面就能实现不同的拥塞控制算法。
- 单调递增的 Packet Number — 使用 Packet Number 代替了 TCP 的 seq。
- 不允许 Reneging — 一个 Packet 只要被 Ack,就认为它一定被正确接收。
- 前向纠错(FEC)
- 更多的 Ack 块和增加 Ack Delay 时间。
- 基于 stream 和 connection 级别的流量控制。
2. 实现了快速握手功能
由于QUIC是基于UDP的,所以QUIC可以实现使用0-RTT或者1-RTT来建立连接,这意味着QUIC可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。0RTT 建连可以说是 QUIC 相比 HTTP2 最大的性能优势。
3. 集成了TLS加密功能。
目前QUIC使用的是TLS1.3,相较于早期版本,TLS1.3有更多的优点,其中最重要的是减少了一个握手所花费的RTT个数。
在完全握手情况下,需要 1-RTT 建立连接。 TLS1.3 恢复会话可以直接发送加密后的应用数据,不需要额外的 TLS 握手,也就是 0-RTT。
但是 TLS1.3 也并不完美。TLS 1.3 的 0-RTT 无法保证前向安全性(Forward secrecy)。简单讲就是,如果当攻击者通过某种手段获取到了 Session Ticket Key,那么该攻击者可以解密以前的加密数据。
要缓解该问题可以通过设置使得与 Session Ticket Key 相关的 DH 静态参数在短时间内过期(一般几个小时)。
4. 实现了HTTP/2中的多路复用功能。
和TCP不同,QUIC实现了同一物理连接上可以有多个独立的逻辑数据流,实现了数据流的单独传输,解决了TCP队头阻塞问题。
5. 连接迁移
TCP 是按照 4 要素(客户端 IP、端口, 服务器 IP、端口)确定一个连接的。而 QUIC 则是让客户端生成一个 Connection ID (64 位)来区别不同连接。只要 Connection ID 不变,连接就不需要重新建立,即便是客户端的网络发生变化。由于迁移客户端继续使用相同的会话密钥来加密和解密数据包,QUIC 还提供了迁移客户端的自动加密验证。
HTTP/3的挑战
通过上面的分析,我们相信在技术层面,HTTP/3是个完美的协议。不过要将HTTP/3应用到实际环境中依然 面临着诸多严峻的挑战,这些挑战主要来自于以下三个方面。
第一,从目前的情况来看,服务器和浏览器端都没有对HTTP/3提供比较完整的支持。Chrome虽然在数年前 就开始支持Google版本的QUIC,但是这个版本的QUIC和官方的QUIC存在着非常大的差异。
第二,部署HTTP/3也存在着非常大的问题。因为系统内核对UDP的优化远远没有达到TCP的优化程度,这 也是阻碍QUIC的一个重要原因。
第三,中间设备僵化的问题。这些设备对UDP的优化程度远远低于TCP,据统计使用QUIC协议时,大约有 3%〜7%的丢包率。
参考文章
尾声
到这,关于HTTP的发展:HTTP/0.9、HTTP/1.0、HTTP/1.1、HTTP/2、HTTP/3就结束了,后续遇到需要补充的会直接在这篇文章里面改,提前感谢大家宝贵的建议!!