概览
HTTP/0.9(超文本传输协议)
-
主要用于学术交流,主要用来在网络之间传递 HTML 超文本的内容,称为超文本传输协议。HTTP/0.9的三个特点:
- 只有一个请求行,GET /index.html, 没有HTTP请求头和请求体
- 服务器没有返回头信息,因为服务端不需要告诉客户端太多信息,只需要返回数据就可以了
- 返回的文件内容是以 ASCII 字符流来传输的,因为都是 HTML 格式的文件,所以使用 ASCII 字节码传输最合适
-
传输过程
1. 客户端根据 IP 地址、端口和服务器三次握手建立连接。
2. 建立连接后,发送 GET /index.html 获取 index.html 文件
3. 服务器接收请求信息后,读取响应的文件,并返回以 ASICII 字符流的数据给客户端
4. 客户端接收完数据,和服务器四次挥手断开连接。
被浏览器推动的 HTTP/1.0
-
由于网络的发展和浏览器的发展,万维网就不局限于学术交流了,带来很多新的需求。
-
在浏览器中不再只是传输 HTML 文件,需要支持多种类型的文件下载,包括 JavaScript、CSS、图片、音频、视频等,引入了请求头和响应头,为 Key-Value 形式保存,在 HTTP 发送请求时,会携带请求头,服务器响应时,会携带响应头。 新增头部信息:
-
浏览器需要知道服务器返回的数据是什么类型的,然后浏览器才能根据不同的数据类型做针对性的处理。
浏览器:accept: text/html 服务器:Content-Type: text/html;charset=UTF-8
-
为了减轻传输性能,服务器会对数据进行压缩后再传输,所以浏览器需要知道服务器压缩的方法。
浏览器:accept-encoding: gzip, deflate, br 服务器:accept-encoding: gzip
-
提供国际化的支持, 需要浏览器告诉服务器它想要什么语言版本的页面。
浏览器:accept-language: zh-CN,zh
-
浏览器需要知道文件的编码类型。
浏览器:acccept-Charset: ISO-8859-1,utf-8 服务器:Content-Type: text/html;charset=UTF-8
-
引入了状态码, 为了减轻服务器的压力,在 HTTP/1.0 中提供了 Cache 机制,用来缓存已经下载过的数据。引入用户代理的字段
user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
Cache-control: public;max-age=120 状态码含义: 1xx消息——请求已被服务器接收,继续处理 2xx成功——请求已成功被服务器接收、理解、并接受 3xx重定向——需要后续操作才能完成这一请求 4xx请求错误——请求含有词法错误或者无法被执行 5xx服务器错误——服务器在处理某个正确请求时发生错误 -
传输过程
HTTP/1.1
- HTTP/1.0 存在的问题
- HTTP/1.0 每进行一次 HTTP 通信,都需要经历建立 TCP 连接、传输 HTTP 数据和断开 TCP 连接三个阶段,每次请求都建立 TCP 连接,会增加大量不必要的开销。
> HTTP/1.1 增加了持久连接的方法,是在一个 TCP 连接上可以传输多个 HTTP 请求,,主要浏览器或服务器没有断开连接,那么该连接就会一直保留。持久连接可以有效减少 TCP 建立连接和断开连接的次数,减少了服务器额外的负担,并提升整体 HTTP 的请求时长。
**Connection: close**关闭持久连接
**Connection: timeout=5**5秒后关闭持久连接
目前浏览器中对于**同一个域名下**,默认允许同时**建立 6 个 TCP 持久连接。**
**队头阻塞**: 持久连接虽然能减少 TCP 的建立和连接次数,但它需要等待前面的请求返回,才能进行下一次请求。如果 TCP 通道中某次请因为某种原因没有及时返回,那么就会阻塞后面所有的请求。
HTTP/1.1试图通过**管线化**的技术来解决队头阻塞的问,管线化是指将多个 HTTP 请求整批提交给服务器的技术,虽然可以整批发送请求,不过服务器依然需要根据请求顺序来回复浏览器的请求。FireFox、Chrome 都做过管线化的试验,但是由于各种原因,它们最终都放弃了管线化技术。
2. 在 HTTP/1.0 中,每个域名绑定了一个唯一的 IP 地址,因此一个服务器只能支持一个域名。
> 但是随着虚拟主机技术的发展,需要实现在一台物理主机上绑定多个虚拟主机,每个虚拟主机都有自己的单独的域名,这些单独的域名都公用同一个 IP 地址。
HTTP/1.1 的请求头中**增加了 Host 字段**,用来表示当前的域名地址,这样服务器就可以根据不同的 Host 值做不同的处理。
3. 在 HTTP/1.0 中,需要在响应头中设置完整的数据大小,如Content-Length: 901,这样浏览器就可以根据设置的数据大小来接收数据。随着服务器端的技术发展,很多页面的内容都是动态生成的,因此在传输数据之前并不知道最终的数据大小,这就导致了浏览器不知道何时会接收完所有的文件数据。
> HTTP/1.1 通过**引入 Chunk transfer 机制**来解决这个问题,服务器会将数据分割成若干个任意大小的数据块,每个数据块发送时会附上上个数据块的长度,最后使用一个零长度的块作为发送数据完成的标志。这样就提供了对动态内容的支持。
4. 在 HTTP/1.1 还引入了客户端 Cookie 机制和安全机制。
- HTTP/1.1 为网络效率做了大量的优化核心的三种方式:
- 增加了持久连接;
- 浏览器为域名最多同时维护 6 个连接;
- 使用 CDN 的实现域名分片机制;
引入 CDN 机制,大大减少了网络耗时。使用单个 TCP 连接耗时:100 * n * RTT,使用 CDN 机制耗时:100 * n * RTT / (CDN个数 * 6)
HTTP/2
-
HTTP/1.1 存在的主要问题:HTTP/1.1对带宽的利用率却并不理想:
-
TCP 的慢启动:TCP 建立连接后,刚开始会采用非常慢的速度,然后慢慢加快发送数据的速度,直到发送数据的速度达到一个理想状态。慢启动推迟了宝贵的首次渲染时间。
-
同时开启多个 TCP 连接,这些连接会竞争固定的带宽。建立多条 TCP 连接时,宽带充足时,每条连接发送或者接收速度会慢慢向上增加;宽带不足时,每条连接又会减慢发送或者接收的速度。 这样会存在一个问题: 有的 TCP 连接下载的是首屏渲染的关键资源(Css 文件、Javascript 文件),有的 TCP 连接下载的是图片、视频、音频等普通资源,TCP 连接不能协商让哪些文件优先下载,这样就可能影响了关键资源的下载。
-
队头阻塞问题:在 HTTP/1.1 中使用持久连接,虽然能共用一个管道,但是在一个管道的同一时刻只能处理一个请求,在当前请求没有结束之前,其他的请求只能处于阻塞状态。如果一个请求因为不确定的因素被阻塞了 5 秒,则后续排队的请求都需要延后 5 秒,在这个等待的过程中,带宽、CPU都被白白浪费了。
在浏览器处理生成页面的过程中,希望提前接收到数据,对数据进行预处理,比如提前接收到图片,就可以提前进行编解码操作,等需要用到这张图片时,就可以直接使用数据,让用户感受到整体速度的提升。
-
-
HTTP/2 多路复用概念 慢启动和 TCP 连接之间相互竞争带宽是由于 TCP 本身的机制导致的,而队头阻塞是由于 HTTP/1.1 的机制导致的。虽然 TCP 有问题,但是我们依然没有换掉 TCP 的能力,所以我们就要想办法去规避 TCP 的慢启动和 TCP 连接之间的竞争问题。 解决方案:一个域名只使用一个 TCP 长连接和消除队头阻塞问题
从图片可以发现每个请求都有唯一的 ID,如 stream1 表示 index.html 的请求,steam4 表示 test.png 的请求,这样浏览器就能随时发请求给服务器。服务器发送的每份数据都有对应的 ID,浏览器收到数据后会筛选出相同 ID 的数据,将其拼接成完成的 HTTP 响应数据。
- HTTP/2 多路复用实现
从图中可以看出,HTTP/2 添加了一个二进制分帧层,分析下 HTTP/2 的请求和接收过程:
1. 浏览器准备请求数据,包括请求行、请求头等信息,如果是 POST 方法,还要有请求体。
2. 数据经过二进制分帧成处理后,会被转换成一个个带有请求 ID 编号的帧,通过协议栈发送给服务器。
3. 服务器接收到所有帧后,会将相同 ID 的帧合并成一条完整的请求信息。
4. 然后服务器处理该条请求,并将处理的响应行、响应头和响应体分别发送至二进制分帧层。
5. 二进制分帧成会将这些响应数据转换成一个个带有请求 ID 编号的帧,经过协议栈发送给浏览器。
6. 浏览器接收到响应帧后,会根据 ID 编号将帧的数据提交给对应的请求。
通过引入二进制分帧层,就实现了 HTTP 的多路复用技术。
-
为什么HTTP/1.1不能实现“多路复用” HTTP/2是基于二进制“帧”的协议,HTTP/1.1是基于“文本分割”解析的协议。 **HTTP/1.1发送请求消息的文本格式:**以换行符分割每一条key:value的内容,解析这种数据往往速度慢且容易出错:一次只能处理一个请求或响应,因为这种以分隔符分割消息的数据,在完成之前不能停止解析;解析这种数据无法预知需要多少内存; **HTTP/2:**基于二进制“帧”的协议,解析前 9 个字节就能准确的知道整个帧期望多少字节数来进行处理信息。
-
HTTP/2 其他特性
-
可以设置请求的优先级:客户端可以在发送请求时,标上该请求的优先级,服务器收到请求后优先处理优先级高的请求。通过HEADERS帧和PRIORITY帧,客户端可以明确的告诉服务端它最需要什么,这是通过声明依赖关系和权重实现的。
-
服务器推送:浏览器请求 HTML 页面后,服务器会知道 HTML 页面会引用的几个重要的 Javascript 文件和 CSS 文件,会把 Javascript 文件和 CSS 文件、HTML 数据一并发送给浏览器。 浏览器解析完 HTML 后,就能直接拿到需要的 CSS 文件和 JavaScript 文件,极大的提升了首屏的加载速度。
Server Push 实现方式
- 利用 HTTP Link 首部
Link: </static/css/styles.css>; rel=preload; as=style, </js/scripts.js>; rel=preload; as=script, </img/logo.png>; rel=preload; as=image-
配置 nginx
- 头部压缩:对请求头和响应头进行了压缩,通过静态霍夫曼编码对传输的标头字段进行编码,要求客户端和服务器同时维护和更新一份相同的静态字典(Static Table)和动态字典(Dynamic Table)。 利用霍夫曼编码,可以在传输时对各个值进行压缩,而利用之前传输值的索引列表, 可以通过传输索引值的方式对重复值进行编码,索引值可用于有效查询和重构完整的标头键值对。
技术原理:
HPACK编码 HPACK 压缩上下文包含一个静态字典(Static Table)和一个动态字典(Dynamic Table): 静态字典(Static Table):常见的头部名称,以及特别常见的头部名称与值的组合
Index Header Name Header Value 1 authority 2 method GET 3 method POST 4 path / 5 path /index.html 6 scheme http 7 scheme https 8 status 200 9 status 204 10 :status 206 11 :status 304 12 :status 400 13 :status 404 14 :status 500 15 accept-charset 16 accept-encoding gzip, deflate 17 accept-language 18 accept-ranges 19 accept 20 access-control-allow-origin 21 age 22 allow 23 authorization 24 cache-control 25 content-disposition 26 content-encoding 27 content-language 28 content-length 29 content-location 30 content-range 31 content-type 32 cookie 33 date 34 etag 35 expect 36 expires 37 from 38 host 39 if-match 40 if-modified-since 41 if-none-match 42 if-range 43 if-unmodified-since 44 last-modified 45 link 46 location 47 max-forwards 48 proxy-authenticate 49 proxy-authorization 50 range 51 referer 52 refresh 53 retry-after 54 server 55 set-cookie 56 strict-transport-security 57 transfer-encoding 58 user-agent 59 vary 60 via 61 www-authenticate 浏览器可以告知服务端,将 cookie: xxxxxxx 添加到动态字典中,这样后续整个键值对就可以使用一个字符表示了。类似的,服务端也可以更新对方的动态字典。需要注意的是,动态字典上下文有关,需要为每个 HTTP/2 连接维护不同的字典。
动态字典(Dynamic Table):可以动态地添加内容; 动态表最初为空,将根据在特定连接内交换的值进行更新。
霍夫曼编码对于静态、动态字典中不存在的内容,还可以使用霍夫曼编码来减小体积。HTTP/2 使用了一份静态哈夫曼码表,也需要内置在客户端和服务端之中。
-
霍夫曼编码过程 霍夫曼编码使用一种特别的方法为信号源中的每个符号设定二进制码。出现频率更大的符号将获得更短的比特,出现频率更小的符号将被分配更长的比特,以此来提高数据压缩率,提高传输效率。
-
同一域名建立一个 TCP 持久连接:通过重用相同的连接,可以显著降低整体协议开销,使用更少的连接还可以减少占用的内存和处理空间,也可以缩短完整连接路径,可以减少开销较大的 TLS 连接数、提升会话重用率,以及从整体上减少所需的客户端和服务器资源。 > 默认情况下,浏览器会针对这些情况使用同一个连接: 同一域名下的资源; 不同域名下的资源,但是满足两个条件:1)解析到同一个 IP;2)使用同一个证书;
-
流控制:每个 HTTP/2 流都有自己的公示的流量窗口,它可以限制另一端发送数据。对于每个流来说,两端都必须告诉对方自己还有足够的空间来处理新的数据,而在该窗口被扩大前,另一端只允许被发送这么多的数据。 WINDOW_UPDATE 帧用来完成这件事情,每个帧告诉对方,发送方想要接收多少字节,它将发送一个 WINDOW_UPDATE 帧以指示其更新后的处理字节能力。
-
可重置:HTTP/1.1中,但一个HTTP信息发送后,很难去中断它(除非断开整个TPC连接),HTTP/2中可以通过发送 RST_STREAME 帧来终止当前传输的消息并且发送一个新的消息。
- HTTP/2 帧类型
-
帧中每个字段保存的信息
帧的字节中保存了不同的信息,前9个字节对于每个帧都是一致的,“服务器”解析HTTP/2的数据帧时只需要解析这些字节,就能准确的知道整个帧期望多少字节数来进行处理信息。
-
HTTP/2 总结 在进行 HTTP/2 网站性能优化时很重要一点是「使用尽可能少的连接数」,本文提到的头部压缩是其中一个很重要的原因:同一个连接上产生的请求和响应越多,动态字典积累得越全,头部压缩效果也就越好。
HTTP/2 头部压缩实现细节
HTTP 头部键值对有如下三种情况:
1)整个头部键值对都在字典中:
使用一个字节就可以表示这个头部了,最左一位固定为 1,之后七位存放键值对在静态或动态字典中的索引。
2)头部名称在字典中,更新动态字典
首先需要使用一个字节表示头部名称:左两位固定为 01,之后六位存放头部名称在静态或动态字典中的索引。
接下来的一个字节第一位 H 表示头部值是否使用了哈夫曼编码,剩余七位表示头部值的长度 L,后续 L 个字节就是头部值的具体内容了。例如下图中索引值为 32(100000),在静态字典中查询可得 cookie;头部值使用了哈夫曼编码(1),长度是 28(0011100);接下来的 28 个字节是 cookie 的值,将其进行哈夫曼解码就能得到具体内容。
客户端或服务端看到这种格式的头部键值对,会将其添加到自己的动态字典中。后续传输这样的内容,就符合第 1 种情况了。
3)头部名称不在字典中,更新动态字典
这种情况与第 2 种情况类似,只是由于头部名称不在字典中,所以第一个字节固定为 01000000;接着申明名称是否使用哈夫曼编码及长度,并放上名称的具体内容;再申明值是否使用哈夫曼编码及长度,最后放上值的具体内容。例如下图中名称的长度是 5(0000101),值的长度是 6(0000110)。对其具体内容进行哈夫曼解码后,可得 pragma: no-cache。
客户端或服务端看到这种格式的头部键值对,会将其添加到自己的动态字典中。后续传输这样的内容,就符合第 1 种情况了。
4)头部名称在字典中,不允许更新动态字典
这种情况与第 2 种情况非常类似,唯一不同之处是:第一个字节左四位固定为 0001,只剩下四位来存放索引了,如下图:
介绍另外一个知识点:对整数的解码。上图中第一个字节为 00011111,并不代表头部名称的索引为 15(1111)。第一个字节去掉固定的 0001,只剩四位可用,将位数用 N 表示,它只能用来表示小于「2 ^ N - 1 = 15」的整数 I。对于 I,需要按照以下规则求值(RFC 7541 中的伪代码,via):
按照这个规则算出索引值为 32(00011111 00010001,15 + 17),代表 cookie。需要注意的是,协议中所有写成(N+)的数字,例如 Index (4+)、Name Length (7+),都需要按照这个规则来编码和解码。
这种格式的头部键值对,不允许被添加到动态字典中(但可以使用哈夫曼编码)。对于一些非常敏感的头部,比如用来认证的 Cookie,这么做可以提高安全性。
5)头部名称不在字典中,不允许更新动态字典
这种情况与第 3 种情况非常类似,唯一不同之处是:第一个字节固定为 00010000。这种情况比较少见。同样,这种格式的头部键值对,也不允许被添加到动态字典中,只能使用哈夫曼编码来减少体积。
协议中还规定了与 4、5 非常类似的另外两种格式:将 4、5 格式中的第一个字节第四位由 1 改为 0 即可。它表示「本次不更新动态词典」,而 4、5 表示「绝对不允许更新动态词典」。
代码实现(HTTP/2 头部解码工具---node-http2 中的 compressor.js)
var Decompressor = require('./compressor').Decompressor;
var testLog = require('bunyan').createLogger({name: 'test'});
var decompressor = new Decompressor(testLog, 'REQUEST');
var buffer = new Buffer('820481634188353daded6ae43d3f877abdd07f66a281b0dae053fad0321aa49d13fda992a49685340c8a6adca7e28102e10fda9677b8d05707f6a62293a9d810020004015309ac2ca7f2c3415c1f53b0497ca589d34d1f43aeba0c41a4c7a98f33a69a3fdf9a68fa1d75d0620d263d4c79a68fbed00177febe58f9fbed00177b518b2d4b70ddf45abefb4005db901f1184ef034eff609cb60725034f48e1561c8469669f081678ae3eb3afba465f7cb234db9f4085aec1cd48ff86a8eb10649cbf', 'hex');
console.log(decompressor.decompress(buffer));
decompressor._table.forEach(function(row, index) {
console.log(index + 1, row[0], row[1]);
});
头部原始数据来自于本文第三张截图,运行结果如下(静态字典只截取了一部分):
{ ':method': 'GET',
':path': '/',
':authority': 'imququ.com',
':scheme': 'https',
'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:41.0) Gecko/20100101 Firefox/41.0',
accept: 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
'accept-language': 'en-US,en;q=0.5',
'accept-encoding': 'gzip, deflate',
cookie: 'v=47; u=6f048d6e-adc4-4910-8e69-797c399ed456',
pragma: 'no-cache' }
1 ':authority' ''
2 ':method' 'GET'
3 ':method' 'POST'
4 ':path' '/'
5 ':path' '/index.html'
6 ':scheme' 'http'
7 ':scheme' 'https'
8 ':status' '200'
... ...
32 'cookie' ''
... ...
60 'via' ''
61 'www-authenticate' ''
62 'pragma' 'no-cache'
63 'cookie' 'u=6f048d6e-adc4-4910-8e69-797c399ed456'
64 'accept-language' 'en-US,en;q=0.5'
65 'accept' 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
66 'user-agent' 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.11; rv:41.0) Gecko/20100101 Firefox/41.0'
67 ':authority' 'imququ.com'
可以看到,这段从 Wireshark 拷出来的头部数据可以正常解码,动态字典也得到了更新(62 - 67)。
HTTP/3
-
传输层 TCP 协议存在的问题:TCP 队头阻塞
-
HTTP/1.1 正常情况下的 TCP 传输数据过程
-
HTTP/1.1 在TCP 丢包状态传输数据过程
在 TCP 传输过程中,由于单个数据包的丢失而造成的阻塞称为 TCP 上的队头阻塞。
-
HTTP/2 正常情况下的 TCP 传输数据过程
在 HTTP/2 中,多个请求是跑在一个 TCP 管道中的,如果其中任意一路数据流中出现了丢包的情况,那么就会阻塞该 TCP 连接中的所有请求。使用 HTTP/1.1 时,浏览器为每个域名开启了 6 个 TCP 连接,如果其中的 1 个 TCP 连接发生了队头阻塞,那么其他的 5 个连接依然可以继续传输数据。有测试数据表明,当系统达到了 2% 的丢包率时,HTTP/1.1 的传输效率反而比 HTTP/2 表现得更好。
-
-
传输层 TCP 协议存在的问题:TCP 建立连接的延时 建立连接需要花掉 3~4 个 RTT。如果浏览器和服务器的物理距离较近, 1 RTT 耗时为 10ms,总共要消耗掉 30~40 毫秒,如果浏览器和服务器的物理距离远, 1 RTT 耗时为 100ms以上,总共要消耗掉 300~400 毫秒。
-
TCP 存在问题,是不是可以通过改进 TCP 协议来解决这些问题呢? TCP 协议僵化:
-
1)中间设备的僵化:中间设备包括路由器、防火墙、NAT、交换机,通常依赖一些很少升级的软件,这些软件使用了大量的 TCP 特性,这些功能被设置之后就很少更新了。如果在客户端升级了 TCP 协议,但是当新协议的数据包经过这些中间设备时,它们可能不理解包的内容,于是这些数据就会被丢弃掉。
-
2)操作系统也是导致 TCP 协议僵化:TCP 协议都是通过操作系统内核来实现的,应用程序只能使用不能修改。通常操作系统的更新都滞后于软件的更新,因此要想自由地更新内核中的 TCP 协议也是非常困难的。
-
-
QUIC 协议
-
实现了类似 TCP 的流量控制、传输可靠性的功能。虽然 UDP 不提供可靠性的传输,但 QUIC 在 UDP 的基础之上增加了一层来保证数据可靠性传输。它提供了数据包重传、拥塞控制以及其他一些 TCP 中存在的特性。
-
集成了 TLS 加密功能。目前 QUIC 使用的是 TLS1.3,相较于早期版本 TLS1.3 有更多的优点,其中最重要的一点是减少了握手所花费的 RTT 个数。
-
实现了 HTTP/2 中的多路复用功能。和 TCP 不同,QUIC 实现了在同一物理连接上可以有多个独立的逻辑数据流(如下图)。实现了数据流的单独传输,就解决了 TCP 中队头阻塞的问题。 QUIC 协议的多路复用:
-
实现了快速握手功能。由于 QUIC 是基于 UDP 的,所以 QUIC 可以实现使用 0-RTT 或者 1-RTT 来建立连接,这意味着 QUIC 可以用最快的速度来发送和接收数据,这样可以大大提升首次打开页面的速度。
-
-
HTTP/3 的挑战
- 从目前的情况来看,服务器和浏览器端都没有对 HTTP/3 提供比较完整的支持。Chrome 虽然在数年前就开始支持 Google 版本的 QUIC,但是这个版本的 QUIC 和官方的 QUIC 存在着非常大的差异。
- 部署 HTTP/3 也存在着非常大的问题。因为系统内核对 UDP 的优化远远没有达到 TCP 的优化程度,这也是阻碍 QUIC 的一个重要原因。
- 中间设备僵化的问题。这些设备对 UDP 的优化程度远远低于 TCP,据统计使用 QUIC 协议时,大约有 3%~7% 的丢包率。
HTTP/3(QUIC 协议)与 HTTP/2比较
-
零 RTT 建立连接
- HTTP/2 的连接需要 3 RTT,如果考虑会话复用,即把第一次握手算出来的对称密钥缓存起来,那么也需要 2 RTT,更进一步的,如果 TLS 升级到 1.3,那么 HTTP/2 连接需要 2 RTT,考虑会话复用则需要 1 RTT。
- HTTP/3 首次连接只需要 1 RTT,后面的连接更是只需 0 RTT,意味着客户端发给服务端的第一个包就带有请求数据,使用 DH 密钥交换算法
-
连接迁移
- TCP 连接基于四元组(源 IP、源端口、目的 IP、目的端口),切换网络时至少会有一个因素发生变化,导致连接发生变化。当连接发生变化时,如果还使用原来的 TCP 连接,则会导致连接失败,就得等原来的连接超时后重新建立连接,所以我们有时候发现切换到一个新网络时,即使新网络状况良好,但内容还是需要加载很久。
- QUIC 连接不以四元组作为标识,而是使用一个 64 位的随机数,这个随机数被称为 Connection ID,即使 IP 或者端口发生变化,只要 Connection ID 没有变化,那么连接依然可以维持。
-
队头阻塞/多路复用
- 队头阻塞:
TCP 的队头阻塞问题
1.TLS 协议也存在队头阻塞问题。TLS 基于 Record 组织数据,将一堆数据放在一起(即一个 Record)加密,加密完后又拆分成多个 TCP 包传输。一般每个 Record 16K,包含 12 个 TCP 包,这样如果 12 个 TCP 包中有任何一个包丢失,那么整个 Record 都无法解密。
2. 是按顺序处理数据包,一旦出现丢包,客户端需要重发数据包,服务端等待收到丢失的数据包,才能继续处理后续的请求,这样就阻塞了该连接中所有的请求。
QUIC 是如何解决队头阻塞问题的呢?:
- QUIC 的传输单元是 Packet,加密单元也是 Packet,整个加密、传输、解密都基于 Packet,这样就能避免 TLS 的队头阻塞问题;
- QUIC 基于 UDP,UDP 的数据包在接收端没有处理顺序,即使中间丢失一个包,也不会阻塞整条连接,其他的资源会被正常处理。
- 多路复用: 由于 QUIC 是为多路复用操作从头设计的,携带个别流的的数据的包丢失时, 通常只影响该流。每个流的帧可以在到达时立即发送给该流, 因此,没有丢失数据的流可以继续重新汇集,并在应用程序中继续进行。
- 队头阻塞:
TCP 的队头阻塞问题
1.TLS 协议也存在队头阻塞问题。TLS 基于 Record 组织数据,将一堆数据放在一起(即一个 Record)加密,加密完后又拆分成多个 TCP 包传输。一般每个 Record 16K,包含 12 个 TCP 包,这样如果 12 个 TCP 包中有任何一个包丢失,那么整个 Record 都无法解密。
2. 是按顺序处理数据包,一旦出现丢包,客户端需要重发数据包,服务端等待收到丢失的数据包,才能继续处理后续的请求,这样就阻塞了该连接中所有的请求。
QUIC 是如何解决队头阻塞问题的呢?:
-
拥塞控制
- TCP 拥塞控制算法
-
慢启动:发送方向接收方发送 1 个单位的数据,收到对方确认后会发送 2 个单位的数据,然后依次是 4 个、8 个……呈指数级增长,这个过程就是在不断试探网络的拥塞程度,超出阈值则会导致网络拥塞;
-
拥塞避免:指数增长不可能是无限的,到达某个限制(慢启动阈值)之后,指数增长变为线性增长;
-
快速重传:发送方每一次发送时都会设置一个超时计时器,超时后即认为丢失,需要重发;
-
快速恢复:在上面快速重传的基础上,发送方重新发送数据时,也会启动一个超时定时器,如果收到确认消息则进入拥塞避免阶段,如果仍然超时,则回到慢启动阶段。
-
- QUIC 重新实现了 TCP 协议的 Cubic 算法进行拥塞控制,并在此基础上做了不少改进
-
热插拔: TCP 中如果要修改拥塞控制策略,需要在系统层面进行操作。 QUIC 修改拥塞控制策略只需要在应用层操作,并且 QUIC 会根据不同的网络环境、用户来动态选择拥塞控制算法。
-
前向纠错FEC: QUIC 使用前向纠错(FEC,Forward Error Correction)技术增加协议的容错性。一段数据被切分为 10 个包后,依次对每个包进行异或运算,运算结果会作为 FEC 包与数据包一起被传输,如果不幸在传输过程中有一个数据包丢失,那么就可以根据剩余 9 个包以及 FEC 包推算出丢失的那个包的数据,这样就大大增加了协议的容错性。
这是符合现阶段网络技术的一种方案,现阶段带宽已经不是网络传输的瓶颈,往返时间才是,所以新的网络传输协议可以适当增加数据冗余,减少重传操作。
-
单调递增的 Packet Number:
- TCP 为了保证可靠性,使用 Sequence Number 和 ACK 来确认消息是否有序到达,但这样的设计存在缺陷。
超时发生后客户端发起重传,后来接收到了 ACK 确认消息,但因为原始请求和重传请求接收到的 ACK 消息一样。
采用原始请求的 ACK,导致计算的采样 RTT 偏大(左图);采用重传请求的 ACK,导致采样 RTT 偏小(右图)。
RTO 是指超时重传时间(Retransmission TimeOut);RTT(Round Trip Time,往返时间),采样 RTT 会影响 RTO(超时时间) 计算。
- QUIC与 Sequence Number 不同的是,Packet Number 严格单调递增,如果 Packet N 丢失了,那么重传时 Packet 的标识不会是 N,而是比 N 大的数字,比如 N + M,这样发送方接收到确认消息时就能方便地知道 ACK 对应的是原始请求还是重传请求。
- TCP 为了保证可靠性,使用 Sequence Number 和 ACK 来确认消息是否有序到达,但这样的设计存在缺陷。
超时发生后客户端发起重传,后来接收到了 ACK 确认消息,但因为原始请求和重传请求接收到的 ACK 消息一样。
采用原始请求的 ACK,导致计算的采样 RTT 偏大(左图);采用重传请求的 ACK,导致采样 RTT 偏小(右图)。
RTO 是指超时重传时间(Retransmission TimeOut);RTT(Round Trip Time,往返时间),采样 RTT 会影响 RTO(超时时间) 计算。
-
ACK Delay: TCP 计算 RTT 时没有考虑接收方接收到数据到发送确认消息之间的延迟,如下图所示,这段延迟即 ACK Delay。QUIC 考虑了这段延迟,使得 RTT 的计算更加准确。
-
更多的 ACK 块:
-
TCP一般来说,接收方收到发送方的消息后都应该发送一个 ACK 回复,表示收到了数据。但每收到一个数据就返回一个 ACK 回复太麻烦,所以一般不会立即回复,而是接收到多个数据后再回复,TCP SACK 最多提供 3 个 ACK block。但有些场景下,比如下载,只需要服务器返回数据就好,但按照 TCP 的设计,每收到 3 个数据包就要“礼貌性”地返回一个 ACK。
-
QUIC 最多可以捎带 256 个 ACK block。在丢包率比较严重的网络下,更多的 ACK block 可以减少重传量,提升网络效率。
- 流量控制:
- TCP会对每个 TCP 连接进行流量控制,流量控制的意思是让发送方不要发送太快,要让接收方来得及接收,不然会导致数据溢出而丢失,TCP 的流量控制主要通过滑动窗口来实现的。可以看出,拥塞控制主要是控制发送方的发送策略,但没有考虑到接收方的接收能力,流量控制是对这部分能力的补齐。
- QUIC 只需要建立一条连接,在这条连接上同时传输多条 Stream,好比有一条道路,两头分别有一个仓库,道路中有很多车辆运送物资
QUIC 的流量控制有两个级别:连接级别(Connection Level)和 Stream 级别(Stream Level)
QUIC 如何实现流量控制
- 单条 Stream 的流量控制
Stream 还没传输数据时,接收窗口(flow control receive window)就是最大接收窗口(flow control receive window),随着接收方接收到数据后,接收窗口不断缩小。在接收到的数据中,有的数据已被处理,而有的数据还没来得及被处理。如下图所示,蓝色块表示已处理数据,黄色块表示未处理数据,这部分数据的到来,使得 Stream 的接收窗口缩小。
随着数据不断被处理,接收方就有能力处理更多数据。当满足 (flow control receive offset - consumed bytes) < (max receive window / 2) 时,接收方会发送 WINDOW_UPDATE frame 告诉发送方你可以再多发送些数据过来。这时 flow control receive offset 就会偏移,接收窗口增大,发送方可以发送更多数据到接收方。
Stream 级别对防止接收端接收过多数据作用有限,更需要借助 Connection 级别的流量控制。理解了 Stream 流量那么也很好理解 Connection 流控。
- Connection 的流量控制
限制 connection 中所有 streams 相加起来的总字节数,防止发送方超过 connection 的缓冲(buffer)容量。
Stream 中,接收窗口(flow control receive window) = 最大接收窗口(max receive window) - 已接收数据(highest received byte offset) ,而对 Connection 来说:接收窗口 = Stream1接收窗口 + Stream2接收窗口 + ... + StreamN接收窗口 。
假设所有 stream 接口窗口为 120:
-
stream 1 的最大接收偏移为 100,可用窗口 = 120 - 60 = 60
-
stream 2 的最大接收偏移为 90,可用窗口 = 120 - 60 = 60
-
stream 3 的最大接收偏移为 110,可用窗口 = 120 - 60 = 60
Connection 的可用窗口 = 60 + 60 + 60 = 180 可用窗口 = stream 1 可用窗口 + stream 2 可用窗口 + stream 3 可用窗口
-
- 单条 Stream 的流量控制
Stream 还没传输数据时,接收窗口(flow control receive window)就是最大接收窗口(flow control receive window),随着接收方接收到数据后,接收窗口不断缩小。在接收到的数据中,有的数据已被处理,而有的数据还没来得及被处理。如下图所示,蓝色块表示已处理数据,黄色块表示未处理数据,这部分数据的到来,使得 Stream 的接收窗口缩小。
-
- TCP 拥塞控制算法
HTTP Headers
- HTTP首部字段根据实际用途被分为以下4种类型。
- **通用首部字段(General Header Fields)**请求报文和响应报文两方都会使用的首部。
- **请求首部字段(Request Header Fields)**从客户端向服务器端发送请求报文时使用的首部。补充了请求的附加内容、客户端信息、响应内容相关优先级等信息。
- **响应首部字段( Response Header Fields)**从服务器端向客户端返回响应报文时使用的首部。补充了响应的附加内容,也会要求客户端附加额外的内容信息。
- **实体首部字段(Entity Header Fields)**针对请求报文和响应报文的实体部分使用的首部。补充了资源内容更新时间等与实体有关的信息。
- HTTP/1.1 首部字段一览
-
通用首部字段
首部字段名 说明 例子 Cache-Control 控制缓存的行为 public、privite、max-age: 0、no-cache、no-store Connection 逐跳首部、连接的管理 keep-alive(继续保持持久连接)close(关闭持久连接) Date 创建报文的日期时间 date: Sun, 20 Mar 2022 09:43:25 GMT Pragma 报文指令 Pragma: no-cache可以应用到http 1.0 和http 1.1,而Cache-Control: no-cache只能应用于http 1.1. Transfer-Encoding 指定报文主体的传输编码方式 Transfer-Encoding:chunked 资源的方式是分块发送 -
请求首部字段
首部字段名 说明 例子 Accept 用户代理可处理的媒体类型 accept: image/avif,image/webp,image/apng,image/svg+xml,image/,/*;q=0.8 Accept-Charset 优先的字符集 Accept-Encoding 优先的内容编码 Accept-Encoding: gzip, deflate 浏览器申明自己接收的编码方法,通常指定压缩方法,是否支持压缩,支持什么压缩方法(gzip,deflate) Accept-Language 优先的语言 Accept-Language:zh-CN,zh;q=0.9 浏览器申明自己接收的语言,q=0.9 表示权重 Host 指定资源所在服务器 host: test-teacher-exam.meishakeji.com origin 指示了请求来自于哪个站点。该字段仅指示服务器名称,并不包含任何路径信息 origin: test-teacher-exam.meishakeji.com If-Match 比较实体标记(ETag) If-Modified-Since 比较资源的更新时间 if-modified-since: Fri, 18 Mar 2022 07:53:25 GMT If-None-Match 比较实体标记(与If-Match相反) if-none-match: W/"62343a75-f1e" Referer 对请求中URI的原始获取方 Referer:www.baidu.com/?tn=6209510… 告诉服务器请求来源 User-Agent 客户端使用的操作系统和浏览器的名称和版本 user-agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36 Authorization 身份认证 token Cookie 身份认证 Cookie cookie: msHeaderSearchTip=true; -
响应首部字段
首部字段名 说明 例子 Accept-Range 是否接受字节范围请求 ETag 资源的匹配信息 ETag: "737060cd8c284d8af7ad3082f209582d" Location 另客户端重定向至指定URI Location: test-teacher-exam.meishakeji.com/ Server 告诉客户端服务器信息 server: nginx/1.16.1 Age 当代理服务器用自己缓存的实体去响应请求时,用该头部表明该实体从产生到现在经过多长时间了。 age: 777 |via 可以用来追踪消息转发情况,防止循环请求 via: cache41.l2st3-1[66,66,304-0,M], cache40.l2st3-1[68,0], cache8.cn1140[0,0,200-0,H], cache2.cn1140[1,0] Vary 决定了对于未来的一个请求头,应该用一个缓存的回复(response)还是向源服务器请求一个新的回复 Vary: User-Agent 在提供给移动端的内容是不同的情况,可用防止你客户端误使用了用于桌面端的缓存,并可帮助Google和其他搜索引擎来发现你的移动端版本的页面 Access-Control-Allow-Origin 指定哪些网站可以跨域资源共享 Access-Control-Allow-Origin: * * 号代表所有网站可以跨域资源共享,如果当前字段为 *那么Access-Control-Allow-Credentials就不能为true Access-Control-Allow-Methods 允许哪些方法来访问 Access-Control-Allow-Methods:GET,POST,PUT,DELETE Access-Control-Allow-Credentials 是否允许发送cookie Access-Control-Allow-Credentials: true 是否允许发送cookie。默认情况下,Cookie不包括在CORS请求之中。设为true,即表示服务器明确许可,Cookie可以包含在请求中,一起发给服务器。这个值也只能设为true,如果服务器不要浏览器发送Cookie,删除该字段即可。如果access-control-allow-origin为*,当前字段就不能为true -
实体首部字段
首部字段名 说明 例子 Allow 资源可支持的Http方法 Content-Encoding 实体主体适用的编码方式 Content-Encoding:gzip Content-Language 实体主体的自然语言 Content-Length 实体主体的大小(字节) Content-Location 替代对应资源的URI Content-Type 资源文件的类型,还有字符编码 Content-Type:text/html;charset=UTF-8 Expires 实体主体过期的日期时间 Expires:Sun, 1 Jan 2000 01:00:00 GMT Last-Modified 资源的最后修改日期时间 Last-Modified: Dec, 26 Dec 2015 17:30:00 GMT
-
说明
本篇文章大量借鉴网络文章,如有侵权,请联系删除