HTTP1.1性能做了哪些优化?

长连接
早期 HTTP/1.0 性能上的⼀个很⼤的问题,那就是每发起⼀个请求,都要新建⼀次TCP连接,而且是串行请求,做了无谓的 TCP 连接建立和断开,增加了通信开销。为了解决上述 TCP 连接问题,HTTP/1.1 提出了长连接的通信方式,也叫持久连接。这种方式的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。 持久连接的特点是,只要任意⼀端没有明确提出断开连接,则保持 TCP 连接状态。
微信截图_20210805232819.png

HTTP/2

HTTP/2 出来的目的是为了改善 HTTP 的性能,HTTP/2兼容了老版本,只是在应用层做了改变,还是基于TCP协议进行传输。HTTP/2 在语法层面做了很多改造,基本改变了 HTTP 报⽂的传输格式。

头部压缩

HTTP/1.1 报文中 Header 部分存在的问题:

  • 含很多固定的字段,比如Cookie、User Agent、Accept 等,这些字段加起来也⾼达几百字节甚至上千字节;
  • 大量的请求和响应的报文里有很多字段值都是重复的,这样会使得大量带宽被这些冗余的数据占用了,所以有必须要避免重复性;
  • 字段是 ASCII 编码的,虽然易于⼈类观察,但效率低,所以有必要改成⼆进制编码;

HTTP/2采用 HPACK 算法进行压缩,该算法主要包括静态字典动态字典Huffman编码。客户端和服务器两端都会建立和维护字典,用长度较小的索引号表示重复的字符串,再用Huffman 编码压缩数据,可达到 50%~90% 的⾼压缩率。

静态编码表

HTTP/2 为⾼频出现在头部的字符串和字段建立了⼀张静态表,它是写⼊到 HTTP/2 框架里的,不会变化的,静态表里共有 61 组
微信截图_20210806091734.png
表中的 Index 表示索引(Key), Header Name 表示字段的名字,Header Value 表示索引对应的 Value

动态编码表

静态表只包含了 61 种⾼频出现在头部的字符串,不在静态表范围内的头部字符串就要自行构建动态表,它的 Index 从 62 起步,会在编码解码的时候随时更新。

比如,第⼀次发送时头部中的 user-agent 字段数据有上百个字节,经过 Huffman 编码发送出去后,客户端和服务器双方都会更新自己的动态表,添加⼀个新的 Index 号 62。那么在下⼀次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 Index 号就好了,因为双⽅都可以根据自己的动态表获取到字段的数据。

二进制数据格式

HTTP/2 将 HTTP/1 的文本格式改成⼆进制格式传输数据,极⼤提高了 HTTP 传输效率,而且⼆进制数据使用位运算能高效解析。
微信截图_20210806093816.png
HTTP/2 把响应报文划分成了两个帧(Frame),并且采用⼆进制来编码。图中的 HEADERS(首部)和 DATA(消息负载) 是帧的类型

并发传输

HTTP/1.1 的实现是基于请求-响应模型的。我们都知道 HTTP/1.1 的实现是基于请求-响应模型的。同⼀个连接中,HTTP 完成⼀个事务(请求与响应),才能处理下⼀个事务,也就是说在发出请求等待响应的过程中,是没办法做其他事情的,如果响应迟迟不来,那么后续的请求是无法发送的。

而 HTTP/2 通过 Stream 这个设计,多个 Stream 复用一条 TCP 连接,达到并发的效果,解决了 HTTP/1.1 队头阻塞的问题,提高了 HTTP 传输的吞吐量
微信截图_20210806095856.png
1 个 TCP 连接包含⼀个或者多个 Stream,Stream 是 HTTP/2 并发的关键技术;Stream 里可以包含 1 个或多个 Message,Message 对应 HTTP/1 中的请求或响应,由 HTTP 头部和包体构成;Message 里包含⼀条或者多个 Frame,Frame 是以⼆进制压缩格式存放 HTTP 中的内容(头部和数据)

在 HTTP/2 连接上,不同 Stream 的帧是可以乱序发送的(因此可以并发不同的 Stream ),因为每个帧的头部会携带 Stream ID 信息,所以接收端可以通过 Stream ID 有序组装成 HTTP 消息,而同⼀ Stream 内部的帧必须是严格有序的。

服务器推送

HTTP/1.1 不支持服务器主动推送资源给客户端,都是由客户端向服务器发起请求后,才能获取到服务器响应的资源。在 HTTP/2 中,客户端在访问 HTML 时,服务器可以直接主动推送 CSS 文件,减少了消息传递的次数。
微信截图_20210806102547.png
客户端发起的请求,必须使用的是奇数号 Stream,服务器主动的推送,使用的是偶数号Stream。服务器在推送资源时,会通过 PUSH_PROMISE 帧传输 HTTP 头部,并通过帧中的 Promised Stream ID 字段告知客户端,接下来会在哪个偶数号 Stream 中发送包体。

总结

  1. 第⼀点,对于常见的 HTTP 头部通过静态表和 Huffman 编码的方式,将体积压缩了近一半,而且针对后续的请求头部,还可以建立动态表,将体积压缩近 90%,⼤⼤提高了编码效率,同时节约了带宽资源。 不过,动态表并非可以无限增⼤,因为动态表是会占用内存的,动态表越⼤,内存也越大,影响服务器总体的并发能力,因此服务器需要限制 HTTP/2 连接时长或者请求次数。
  2. 第⼆点,HTTP/2 实现了 Stream 并发,多个 Stream 只需复用 1 个 TCP 连接,节约了 TCP 和 TLS 握⼿时间,以及减少了 TCP 慢启动阶段对流量的影响。不同的 Stream ID 才可以并发,即时乱序发送帧也没问题,但是同⼀个Stream 里的帧必须严格有序。
  3. 服务器⽀持主动推送资源,大大提升了消息的传输性能,服务器推送资源时,会先发送 PUSH_PROMISE 帧,告诉客户端接下来在哪个 Stream 发送资源,然后用偶数号 Stream 发送资源给客户端。

HTTP/2存在的问题:
HTTP/2 通过 Stream 的并发能力,解决了 HTTP/1 队头阻塞的问题,看似很完美了,但是 HTTP/2 还是存在“队头阻塞”的问题,只不过问题不是在 HTTP 这⼀层面,而是在 TCP 这⼀层。

HTTP/2 是基于 TCP 协议来传输数据的,TCP 是字节流协议,TCP 层必须保证收到的字节数据是完整且连续的, 这样内核才会将缓冲区里的数据返回给 HTTP 应用,那么当前1 个字节数据没有到达时,后收到的字节数据只能存放在内核缓冲区里,只有等到这 1 个字节数据到达时,HTTP/2 应用层才能从内核中拿到数据,这就是 HTTP/2 队头阻塞问题。