2121- * 内容形式变多样了* ,从单纯到文本内容,到图片、视频、音频等内容;
2222- * 实时性要求变高了* ,对页面的实时性要求的应用越来越多;
2323
24- 这些变化带来的最大性能问题就是 ** HTTP/1.1 的高延迟** ,延迟高必然影响的就是用户体验。主要原因如下几个:
24+ 这些变化带来的最大性能问题就是 ** HTTP/1.1 的高延迟** ,延迟高必然影响的就是用户体验。主要原因如下几个:
2525
2626- * 延迟难以下降* ,虽然现在网络的「带宽」相比以前变多了,但是延迟降到一定幅度后,就很难再下降了,说白了就是到达了延迟的下限;
2727- * 并发连接有限* ,谷歌浏览器最大并发连接数是 6 个,而且每一个连接都要经过 TCP 和 TLS 握手耗时,以及 TCP 慢启动过程给流量带来的影响;
2828- * 队头阻塞问题* ,同一连接只能在完成一个 HTTP 事务(请求和响应)后,才能处理下一个事务;
29- - * HTTP 头部巨大且重复* ,由于 HTTP 协议是无状态的,每一个请求都得携带 HTTP 头部,特别是对于有携带 cookie 的头部,而 cookie 的大小通常很大;
29+ - * HTTP 头部巨大且重复* ,由于 HTTP 协议是无状态的,每一个请求都得携带 HTTP 头部,特别是对于有携带 Cookie 的头部,而 Cookie 的大小通常很大;
3030- * 不支持服务器推送消息* ,因此当客户端需要获取通知时,只能通过定时器不断地拉取消息,这无疑浪费大量了带宽和服务器资源。
3131
32- 为了解决 HTTP/1.1 性能问题,具体的优化手段你可以看这篇文章「[ HTTP/1.1如何优化 ?] ( https://xiaolincoding.com/network/2_http/http_optimize.html ) 」,这里我举例几个常见的优化手段:
32+ 为了解决 HTTP/1.1 性能问题,具体的优化手段你可以看这篇文章「[ HTTP/1.1 如何优化 ?] ( https://xiaolincoding.com/network/2_http/http_optimize.html ) 」,这里我举例几个常见的优化手段:
3333
3434- 将多张小图合并成一张大图供浏览器 JavaScript 来切割使用,这样可以将多个请求合并成一个请求,但是带来了新的问题,当某张小图片更新了,那么需要重新请求大图片,浪费了大量的网络带宽;
35- - 将图片的二进制数据通过 base64 编码后,把编码数据嵌入到 HTML 或 CSS 文件中,以此来减少网络请求次数;
36- - 将多个体积较小的 JavaScript 文件使用 webpack 等工具打包成一个体积更大的 JavaScript 文件,以一个请求替代了很多个请求,但是带来的问题,当某个 js 文件变化了,需要重新请求同一个包里的所有 js 文件;
35+ - 将图片的二进制数据通过 Base64 编码后,把编码数据嵌入到 HTML 或 CSS 文件中,以此来减少网络请求次数;
36+ - 将多个体积较小的 JavaScript 文件使用 Webpack 等工具打包成一个体积更大的 JavaScript 文件,以一个请求替代了很多个请求,但是带来的问题,当某个 js 文件变化了,需要重新请求同一个包里的所有 js 文件;
3737- 将同一个页面的资源分散到不同域名,提升并发连接上限,因为浏览器通常对同一域名的 HTTP 连接最大只能是 6 个;
3838
3939
40- 尽管对 HTTP/1.1 协议的优化手段如此之多,但是效果还是不尽人意,因为这些手段都是对 HTTP/1.1 协议的“外部”做优化,** 而一些关键的地方是没办法优化的,比如请求-响应模型、头部巨大且重复、并发连接耗时、服务器不能主动推送等,要改变这些必须重新设计 HTTP 协议,于是 HTTP/2 就出来了!**
40+ 尽管对 HTTP/1.1 协议的优化手段如此之多,但是效果还是不尽人意,因为这些手段都是对 HTTP/1.1 协议的“外部”做优化,** 而一些关键的地方是没办法优化的,比如请求-响应模型、头部巨大且重复、并发连接耗时、服务器不能主动推送等,要改变这些必须重新设计 HTTP 协议,于是 HTTP/2 就出来了!**
4141
4242
4343---
4444
4545## 兼容 HTTP/1.1
4646
47- HTTP/2 出来的目的是为了改善 HTTP 的性能。协议升级有一个很重要的地方,就是要** 兼容** 老版本的协议,否则新协议推广起来就相当困难,所幸 HTTP/2 做到了兼容 HTTP/1.1 。
47+ HTTP/2 出来的目的是为了改善 HTTP 的性能。协议升级有一个很重要的地方,就是要** 兼容** 老版本的协议,否则新协议推广起来就相当困难,所幸 HTTP/2 做到了兼容 HTTP/1.1。
4848
4949那么,HTTP/2 是怎么做的呢?
5050
@@ -56,11 +56,11 @@ HTTP/2 出来的目的是为了改善 HTTP 的性能。协议升级有一个很
5656
5757## 头部压缩
5858
59- HTTP 协议的报文是由「Header + Body」构成的,对于 Body 部分,HTTP/1.1 协议可以使用头字段 「Content-Encoding」指定 Body 的压缩方式,比如用 gzip 压缩,这样可以节约带宽,但报文中的另外一部分 Header,是没有针对它的优化手段。
59+ HTTP 协议的报文是由「Header + Body」构成的,对于 Body 部分,HTTP/1.1 协议可以使用头字段 「Content-Encoding」指定 Body 的压缩方式,比如用 gzip 压缩,这样可以节约带宽,但报文中的另外一部分 Header,是没有针对它的优化手段。
6060
6161HTTP/1.1 报文中 Header 部分存在的问题:
6262
63- - 含很多固定的字段,比如Cookie 、User Agent、Accept 等,这些字段加起来也高达几百字节甚至上千字节,所以有必要** 压缩** ;
63+ - 含很多固定的字段,比如 Cookie 、User Agent、Accept 等,这些字段加起来也高达几百字节甚至上千字节,所以有必要** 压缩** ;
6464- 大量的请求和响应的报文里有很多字段值都是重复的,这样会使得大量带宽被这些冗余的数据占用了,所以有必须要** 避免重复性** ;
6565- 字段是 ASCII 编码的,虽然易于人类观察,但效率低,所以有必要改成** 二进制编码** ;
6666
@@ -91,7 +91,7 @@ HTTP/2 为高频出现在头部的字符串和字段建立了一张**静态表**
9191server: nghttpx\r\n
9292```
9393
94- 算上冒号空格和末尾的\r\n,共占用了 17 字节,** 而使用了静态表和 Huffman 编码,可以将它压缩成 8 字节,压缩率大概 47 %** 。
94+ 算上冒号空格和末尾的` \r\n ` ,共占用了 17 字节,** 而使用了静态表和 Huffman 编码,可以将它压缩成 8 字节,压缩率大概 47%** 。
9595
9696我抓了个 HTTP/2 协议的网络包,你可以从下图看到,高亮部分就是 ` server ` 头部字段,只用了 8 个字节来表示 ` server ` 头部数据。
9797
@@ -133,7 +133,7 @@ HTTP/2 头部由于基于**二进制编码**,就不需要冒号空格和末尾
133133静态表只包含了 61 种高频出现在头部的字符串,不在静态表范围内的头部字符串就要自行构建** 动态表** ,它的 Index 从 ` 62 ` 起步,会在编码解码的时候随时更新。
134134
135135
136- 比如,第一次发送时头部中的「` user-agent ` 」字段数据有上百个字节,经过 Huffman 编码发送出去后,客户端和服务器双方都会更新自己的动态表,添加一个新的 Index 号 62。** 那么在下一次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 Index 号就好了,因为双方都可以根据自己的动态表获取到字段的数据** 。
136+ 比如,第一次发送时头部中的「` User-Agent ` 」字段数据有上百个字节,经过 Huffman 编码发送出去后,客户端和服务器双方都会更新自己的动态表,添加一个新的 Index 号 62。** 那么在下一次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 Index 号就好了,因为双方都可以根据自己的动态表获取到字段的数据** 。
137137
138138所以,使得动态表生效有一个前提:** 必须同一个连接上,重复传输完全相同的 HTTP 头部** 。如果消息字段在 1 个连接上只发送了 1 次,或者重复传输时,字段总是略有变化,动态表就无法被充分利用了。
139139
@@ -151,7 +151,7 @@ HTTP/2 头部由于基于**二进制编码**,就不需要冒号空格和末尾
151151
152152HTTP/2 厉害的地方在于将 HTTP/1 的文本格式改成二进制格式传输数据,极大提高了 HTTP 传输效率,而且二进制数据使用位运算能高效解析。
153153
154- 你可以从下图看到,HTTP/1.1 的响应 和 HTTP/2 的区别:
154+ 你可以从下图看到,HTTP/1.1 的响应和 HTTP/2 的区别:
155155
156156![ ] ( https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/网络/http2/二进制帧.png )
157157
@@ -170,7 +170,7 @@ Header: :status: 200 OK 的编码内容为:1000 1000,那么表达的含义
170170![ ] ( https://cdn.xiaolincoding.com/gh/xiaolincoder/network/http/index.png )
171171
1721721 . 最前面的 1 标识该 Header 是静态表中已经存在的 KV。
173- 2 . 我们再回顾一下之前的静态表内容,“:status: 200 ok”其静态表编码是8,即1000 。
173+ 2 . 我们再回顾一下之前的静态表内容,“:status: 200 OK”其静态表编码是 8,即 1000 。
174174
175175因此,整体加起来就是 1000 1000。
176176
@@ -227,23 +227,23 @@ HTTP/2 **二进制帧**的结构如下图:
227227
228228客户端和服务器** 双方都可以建立 Stream** ,因为服务端可以主动推送资源给客户端, 客户端建立的 Stream 必须是奇数号,而服务器建立的 Stream 必须是偶数号。
229229
230- 比如下图,Stream 1 是客户端向服务端请求的资源,属于客户端建立的 Stream,所以该 Stream 的 ID 是奇数(数字1 );Stream 2 和 4 都是服务端主动向客户端推送的资源,属于服务端建立的 Stream,所以这两个 Stream 的 ID 是偶数(数字 2 和4 )。
230+ 比如下图,Stream 1 是客户端向服务端请求的资源,属于客户端建立的 Stream,所以该 Stream 的 ID 是奇数(数字 1 );Stream 2 和 4 都是服务端主动向客户端推送的资源,属于服务端建立的 Stream,所以这两个 Stream 的 ID 是偶数(数字 2 和 4 )。
231231
232232![ ] ( https://img-blog.csdnimg.cn/83445581dafe409d8cfd2c573b2781ac.png )
233233
234234同一个连接中的 Stream ID 是不能复用的,只能顺序递增,所以当 Stream ID 耗尽时,需要发一个控制帧 ` GOAWAY ` ,用来关闭 TCP 连接。
235235
236236在 Nginx 中,可以通过 ` http2_max_concurrent_Streams ` 配置来设置 Stream 的上限,默认是 128 个。
237237
238- HTTP/2 通过 Stream 实现的并发,比 HTTP/1.1 通过 TCP 连接实现并发要牛逼的多,** 因为当 HTTP/2 实现 100 个并发 Stream 时,只需要建立一次 TCP 连接,而 HTTP/1.1 需要建立 100 个 TCP 连接,每个 TCP 连接都要经过TCP 握手、慢启动以及 TLS 握手过程,这些都是很耗时的。**
238+ HTTP/2 通过 Stream 实现的并发,比 HTTP/1.1 通过 TCP 连接实现并发要牛逼的多,** 因为当 HTTP/2 实现 100 个并发 Stream 时,只需要建立一次 TCP 连接,而 HTTP/1.1 需要建立 100 个 TCP 连接,每个 TCP 连接都要经过 TCP 握手、慢启动以及 TLS 握手过程,这些都是很耗时的。**
239239
240240HTTP/2 还可以对每个 Stream 设置不同** 优先级** ,帧头中的「标志位」可以设置优先级,比如客户端访问 HTML/CSS 和图片资源时,希望服务器先传递 HTML/CSS,再传图片,那么就可以通过设置 Stream 的优先级来实现,以此提高用户体验。
241241
242242## 服务器主动推送资源
243243
244244HTTP/1.1 不支持服务器主动推送资源给客户端,都是由客户端向服务器发起请求后,才能获取到服务器响应的资源。
245245
246- 比如,客户端通过 HTTP/1.1 请求从服务器那获取到了 HTML 文件,而 HTML 可能还需要依赖 CSS 来渲染页面,这时客户端还要再发起获取 CSS 文件的请求,需要两次消息往返,如下图左边部分:
246+ 比如,客户端通过 HTTP/1.1 请求从服务器那获取到了 HTML 文件,而 HTML 可能还需要依赖 CSS 来渲染页面,这时客户端还要再发起获取 CSS 文件的请求,需要两次消息往返,如下图左边部分:
247247
248248![ ] ( https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost4@main/网络/http2/push.png )
249249
@@ -278,7 +278,7 @@ HTTP/2 协议其实还有很多内容,比如流控制、流状态、依赖关
278278
279279不过,动态表并非可以无限增大, 因为动态表是会占用内存的,动态表越大,内存也越大,容易影响服务器总体的并发能力,因此服务器需要限制 HTTP/2 连接时长或者请求次数。
280280
281- 第二点,** HTTP/2 实现了 Stream 并发** ,多个 Stream 只需复用 1 个 TCP 连接,节约了 TCP 和 TLS 握手时间,以及减少了 TCP 慢启动阶段对流量的影响。不同的 Stream ID 可以并发,即使乱序发送帧也没问题,比如发送 A 请求帧1 -> B 请求帧1 -> A 请求帧2 -> B 请求帧2,但是同一个 Stream 里的帧必须严格有序。
281+ 第二点,** HTTP/2 实现了 Stream 并发** ,多个 Stream 只需复用 1 个 TCP 连接,节约了 TCP 和 TLS 握手时间,以及减少了 TCP 慢启动阶段对流量的影响。不同的 Stream ID 可以并发,即使乱序发送帧也没问题,比如发送 A 请求帧 1 -> B 请求帧 1 -> A 请求帧 2 -> B 请求帧2,但是同一个 Stream 里的帧必须严格有序。
282282
283283另外,可以根据资源的渲染顺序来设置 Stream 的** 优先级** ,从而提高用户体验。
284284
@@ -288,7 +288,7 @@ HTTP/2 通过 Stream 的并发能力,解决了 HTTP/1 队头阻塞的问题,
288288
289289** HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的,这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当「前 1 个字节数据」没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。**
290290
291- 有没有什么解决方案呢?既然是 TCP 协议自身的问题,那干脆放弃 TCP 协议,转而使用 UDP 协议作为传输层协议,这个大胆的决定, HTTP/3 协议做了!
291+ 有没有什么解决方案呢?既然是 TCP 协议自身的问题,那干脆放弃 TCP 协议,转而使用 UDP 协议作为传输层协议,这个大胆的决定,HTTP/3 协议做了!
292292
293293![ ] ( https://cdn.xiaolincoding.com/gh/xiaolincoder/ImageHost/计算机网络/HTTP/27-HTTP3.png )
294294
0 commit comments