避免HTTP请求并不意味着就不进行交互,而是使用缓存技术来减少通过网络获得服务器的响应。
HTTP缓存两种
(1)强制缓存
(2)协商缓存
缓存部分已经总结过了,可以看这个链接(建议先看HTTP复习(一)再看本篇文章)→HTTP复习(一)
什么是重定向请求?
服务器上的一个资源可能由于迁移、维护等原因从 url1 移至 url2 后,而客户端不知情,它还是继续请求 url1,这时服务器不能粗暴地返回错误,而是通过 302 响应码和 Location 头部,告诉客户端该资源已经迁移至 url2 了,于是客户端需要再发送 url2 请求以获得服务器的资源。
1、性能最差:
2、提高一定性能:
3、让代理服务器知道要重定向:
如果把多个访问小文件的请求合并成一个大的请求,虽然传输的总资源还是一样,但是减少请求,也就意味着减少了重复发送的 HTTP 头部。为了防止单个请求的队头阻塞问题,所以一般浏览器会同时发起 5-6 个请求,每一个请求都是不同的 TCP 连接,那么如果合并了请求,也就会减少 TCP 连接的数量,因而省去了 TCP 握手和慢启动过程耗费的时间。
合并请求的方式就是合并资源,以一个大资源的请求替换多个小资源的请求。
但是这样的合并请求会带来新的问题,当大资源中的某一个小资源发生变化后,客户端必须重新下载整个完整的大资源文件,这显然带来了额外的网络消耗。
不要解析html文件时候,遇到一个资源的URL就发送HTTP请求请求该资源。我们可以按需请求,用到了我们再去请求。不用到就不用
在用户看来,浏览一个网页时候,往下滑动页面时才会加载新内容,而不是一上来就全部加载出来。
通过压缩数据,减少HTTP响应报文的大小
无损压缩是指资源经过压缩后,信息不被破坏,还能完全恢复到压缩前的原样,适合用在文本文件、程序可执行文件、程序源代码。
客户端支持的压缩算法,会在 HTTP 请求中通过头部中的 Accept-Encoding 字段告诉服务器。
压缩后,通过响应头部中的 content-encoding 字段告诉客户端该资源使用的压缩算法。
有损压缩主要将次要的数据舍弃,牺牲一些质量来减少数据量、提高压缩比,这种方法经常用于压缩多媒体数据,比如音频、视频、图片。
可以通过 HTTP 请求头部中的 Accept 字段里的「 q 质量因子」,告诉服务器期望的资源质量。
将同一个页面的资源分散到不同域名,提升并发连接上限,因为浏览器通常对同一域名的 HTTP 连接最大只能是 6 个;
HTTP/1.1存在的问题:
1、报文Header部分上,每一个报文都有完全相同的字段,每个都传输一样的值,会有浪费
2、有一些Header字段是固定的
3、字段是ASCII编码,方便人看了,但是对于计算机没啥用,还浪费空间,改成二进制编码。
HTTP/2 使用了 HPACK算法 ,其中主要包含三部分
1、静态字典
2、动态字典
3、哈夫曼编码(压缩算法)
客户端和服务器两端都会建立和维护「字典」,用长度较小的索引号表示重复的字符串,对于动态变化的部分,再用 Huffman 编码压缩数据。
静态表只包含了 61 种高频出现在头部的字符串,不在静态表范围内的头部字符串就要自行构建动态表,它的 Index 从 62 起步,会在编码解码的时候随时更新。
比如,第一次发送时头部中的「user-agent 」字段数据有上百个字节,经过 Huffman 编码发送出去后,客户端和服务器双方都会更新自己的动态表,添加一个新的 Index 号 62。那么在下一次发送的时候,就不用重复发这个字段的数据了,只用发 1 个字节的 Index 号就好了,因为双方都可以根据自己的动态表获取到字段的数据。
所以,使得动态表生效有一个前提:必须同一个连接上,重复传输完全相同的 HTTP 头部。如果消息字段在 1 个连接上只发送了 1 次,或者重复传输时,字段总是略有变化,动态表就无法被充分利用了。
理想很美好,现实很骨感。动态表越大,占用的内存也就越大,如果占用了太多内存,是会影响服务器性能的,因此 Web 服务器都会提供类似 http2_max_requests 的配置,用于限制一个连接上能够传输的请求数量,避免动态表无限增大,请求数量到达上限后,就会关闭 HTTP/2 连接来释放内存。
大体就是根据不同字母出现次数的不同,构建二叉树,保证每个字母都能用最短的不重复的比特位表示,从而达到压缩的效果。
总结,具体如何压缩 Header Name: Header Value
(1)Header Name 和 Header Value都存在
Index(一字节)
(2)Header Name存在 和 Header Value不存在
Index(一字节) + 表示Header Value长度字节(首bit位表示Value部分进行哈夫曼编码)+Header Value编码的后的值
(3)都不存在
将整体编码,记录在动态表
将HTTP/1.1的文本格式变成了二进制格式传输数据,极大提高了HTTP传输效率。并且二进制数据使用位运算能高效解析。
一个HTTP请求或者响应报文成为一个Message。
而一个Message由两类Frame(Header Frame & Data Frame),一个Message可以有多个Header Frame和多个Data Frame中。
Header Frame的Payload承载的是经过HPACK算法加工后的报头数据
Data Frame的Payload承载的是经过HPACK算法加工后的报文数据
帧头(Frame Header)很小,只有 9 个字节
帧开头的前 3 个字节表示帧数据(Frame Playload)的长度。
帧类型,HTTP/2 总共定义了 10 种类型的帧,一般分为数据帧和控制帧两类,如下表格:
标志位,可以保存 8 个标志位,用于携带简单的控制信息。如:
流标识符(Stream ID):最高位被保留不用。用来标识该 Frame 属于哪个 Stream,接收方可以根据这个信息从乱序的帧里找到相同 Stream ID 的帧,从而有序组装信息。
帧数据:存放经过HPACK算法压缩过的HTTP报头或者数据(具体由帧类型决定)
为了解决HTTP的队头阻塞的问题,设计了Stream
那是如何做到并发的呢?
一个TCP链接中的多个Stream进行单向传输。 客户端建立的 Stream 必须是奇数号,而服务器建立的 Stream 必须是偶数号。同一个连接中的 Stream ID 是不能复用的,只能顺序递增,所以当 Stream ID 耗尽时,需要发一个控制帧 GOAWAY,用来关闭 TCP 连接。在 Nginx 中,可以通过 http2_max_concurrent_Streams 配置来设置 Stream 的上限,默认是 128 个。
不同Stream之间可以乱序传输,只需要通过Stream ID,即可区分到底来源于哪一个Stream。
但是Stream内部每一个Frame必须是有序的,一旦一个Frame阻塞,那么这个Stream就会发生阻塞。但是不会影响其他Stream。
HTTP/2 通过 Stream 实现的并发,比 HTTP/1.1 通过 TCP 连接实现并发要牛逼的多,因为当 HTTP/2 实现 100 个并发 Stream 时,只需要建立一次 TCP 连接,而 HTTP/1.1 需要建立 100 个 TCP 连接,每个 TCP 连接都要经过TCP 握手、慢启动以及 TLS 握手过程,这些都是很耗时的。
HTTP/1.1每次都要客户端主动发起链接请求新资源。HTTP/2时候,服务器就可以主动发送给客户端其需要的内容。
那 HTTP/2 的推送是怎么实现的?
客户端发起的请求,必须使用的是奇数号 Stream,服务器主动的推送,使用的是偶数号 Stream。服务器在推送资源时,会通过 PUSH_PROMISE 帧传输 HTTP 头部,并通过帧中的 Promised Stream ID 字段告知客户端,接下来会在哪个偶数号 Stream 中发送包体。
(1)TCP的队头阻塞
(2)TCP和TLS握手带来的延迟
(3)网络迁移的重新连接
HTTP/3 不仅仅只是简单将传输协议替换成了 UDP,还基于 UDP 协议在「应用层」实现了 QUIC 协议,它具有类似 TCP 的连接管理、拥塞窗口、流量控制的网络特性,相当于将不可靠传输的 UDP 协议变成“可靠”的了,所以不用担心数据包丢失的问题。
QUCI的优点:
(1)无队头阻塞
(2)更快的链接建立
(3)连接迁移
QUIC 协议也有类似 HTTP/2 Stream 与多路复用的概念,也是可以在同一条连接上并发传输多个 Stream,Stream 可以认为就是一条 HTTP 请求。
由于 QUIC 使用的传输协议是 UDP,UDP 不关心数据包的顺序,如果数据包丢失,UDP 也不关心。
不过 QUIC 协议会保证数据包的可靠性,每个数据包都有一个序号唯一标识。当某个流中的一个数据包丢失了,即使该流的其他数据包到达了,数据也无法被 HTTP/3 读取,直到 QUIC 重传丢失的报文,数据才会交给 HTTP/3。
而其他流的数据报文只要被完整接收,HTTP/3 就可以读取到数据。这与 HTTP/2 不同,HTTP/2 只要某个流中的数据包丢失了,其他流也会因此受影响。
所以,QUIC 连接上的多个 Stream 之间并没有依赖,都是独立的,某个流发生丢包了,只会影响该流,其他流不受影响。
HTTP/3 在传输数据前虽然需要 QUIC 协议握手,这个握手过程只需要 1 RTT,握手的目的是为确认双方的「连接 ID」,连接迁移就是基于连接 ID 实现的。
但是 HTTP/3 的 QUIC 协议并不是与 TLS 分层,而是QUIC 内部包含了 TLS,它在自己的帧会携带 TLS 里的“记录”,再加上 QUIC 使用的是 TLS1.3,因此仅需 1 个 RTT 就可以「同时」完成建立连接与密钥协商,甚至在第二次连接的时候,应用数据包可以和 QUIC 握手信息(连接信息 + TLS 信息)一起发送,达到 0-RTT 的效果。
HTTP/3 当会话恢复时,有效负载数据与第一个数据包一起发送,可以做到 0-RTT:
QUIC 协议没有用四元组的方式来“绑定”连接,而是通过连接 ID来标记通信的两个端点,客户端和服务器可以各自选择一组 ID 来标记自己,因此即使移动设备的网络变化后,导致 IP 地址变化了,只要仍保有上下文信息(比如连接 ID、TLS 密钥等),就可以“无缝”地复用原连接,消除重连的成本,没有丝毫卡顿感,达到了连接迁移的功能。
HTTP/3 在头部压缩算法这一方面也做了升级,升级成了 QPACK。与 HTTP/2 中的 HPACK 编码方式相似,HTTP/3 中的 QPACK 也采用了静态表、动态表及 Huffman 编码。
对于静态表的变化,HTTP/2 中的 HPACK 的静态表只有 61 项,而 HTTP/3 中的 QPACK 的静态表扩大到 91 项。
HTTP/2 和 HTTP/3 的 Huffman 编码并没有多大不同,但是动态表编解码方式不同。
所谓的动态表,在首次请求-响应后,双方会将未包含在静态表中的 Header 项更新各自的动态表,接着后续传输时仅用 1 个数字表示,然后对方可以根据这 1 个数字从动态表查到对应的数据,就不必每次都传输长长的数据,大大提升了编码效率。
可以看到,动态表是具有时序性的,如果首次出现的请求发生了丢包,后续的收到请求,对方就无法解码出 HPACK 头部,因为对方还没建立好动态表,因此后续的请求解码会阻塞到首次请求中丢失的数据包重传过来。HPACK的队头阻塞问题
那QPACK又是如何解决这个问题的呢?
QUIC 会有两个特殊的单向流,这两个单向流的用法:
QPACK Encoder Stream, 用于将一个字典(key-value)传递给对方,比如面对不属于静态表的 HTTP 请求头部,客户端可以通过这个 Stream 发送字典;
QPACK Decoder Stream,用于响应对方,告诉它刚发的字典已经更新到自己的本地动态表了,后续就可以使用这个字典来编码了。
这两个特殊的单向流是用来同步双方的动态表,编码方收到解码方更新确认的通知后,才使用动态表编码 HTTP 头部。
本文总结于小林coding