HTTP - HTTP/2 知识点

引言

在《图解HTTP》的读书笔记[《图解HTTP》- HTTP协议历史发展(重点)]当中介绍了一部分关于HTTP/2的内容,但是内容比较简短没有过多深入,本文对于HTTP/2 协议做一个更深入的介绍。

概览

HTTP1.X 有两个主要的缺点:安全不足性能不高

所谓安全不足,是指HTTP1.X 大部分时候使用了明文传输,所以很多时候黑客可以通过抓包报文的方式对于网络数据进行监控和尝试破解,为了安全传输数据,HTTP通常和TLS组合实现网络安全连接。

性能不高则指的是HTTP在请求传输中会传输大量的重复字段,Body的数据可以通过GZIP进行压缩。这达到了可以勉强接收传输效率,但是Header头部字段依旧非常臃肿和低效,并且HTTP1.X 后续也没有效的头部压缩手段,HTTP/2 借用了哈夫曼编码对于Header进行高效压缩,提高传输效率。

除了上面的问题,HTTP1.X中最大的问题是队头阻塞,HTTP1.X中浏览器对于同一域名的并发连接访问此时是有限的,所以常常会导致只有个位数的连接可以正常工作,后续的连接都会被阻塞。

HTTP/2 解决队头阻塞是以 HTTP1.X 管道化的为基础拓展,它使用了二进制流和帧概念解决应用层队头阻塞。应用层的阻塞被解决便是实现流并发传输

为了控制资源的资源的获取顺序,HTTP在并发传输的基础上实现请求优先级以及流量控制,流的流量控制是考虑接收方是否具备接收能力。

在发送方存在WINDOWS流量窗口,而接收方可以通过一个叫做WINDOW_UPDATE帧限制发送方的传输长度。

要理解HTTP/2的细节需要有一个宏观的概念:为了提高效率,HTTP/2整体都在向着TCP协议贴近

以上就是对于HTTP/2升级的模糊理解,HTTP/2 的改进从整体上分为下面几个部分:

  • 兼容HTTP1.X
  • 应用层队头阻塞解决
  • 并发传输
  • 多路复用
  • 二进制帧
  • 服务器推送
  • HPACK/头部压缩
  • 请求优先级
  • 补充
    - 连接前言
    - 流和管道化关系
    - 请求头字段约束

思维导图

https://www.mubucm.com/doc/3kTM1b8PGV5

兼容HTTP1.X

HTTP和TLS协议一样背着巨大的历史包袱,所以不能在结构上做出过多的改动,HTTP/2为了进行推广也必须要进行前后兼容,而兼容HTTP1.X 则引导出下面三个点:

  • HTTP协议头平滑过渡
  • 应用层协议改变
  • 基本传输格式的保证

HTTP协议头平滑过渡

所谓的平滑过渡指的是协议头的识别依然是 HTTP开头,不管是HTTP1 还是 HTTP/2,甚至是HTTP3,都将会沿用http开头的协议名进行向后兼容。

应用层协议改变

HTTP/2只改变了应用层并没有改变TCP层和IP层,所以数据依然是通过TCP报文的方式进行传输,经过TCP握手和HTTP握手完成。

基本传输格式的保证

HTTP1.X中的请求报文格式如下,结合来说请求报文可以总结为下面的格式:

  • 请求行
  • 请求首部字段和其他字段
  • 空行
  • 请求负载

二进制帧(Stream)

二进制帧是HTTP/2的“语法”变动,HTTP/2的传输格式由明文转为了二进制格式, 属于向 TCP/IP 协议“靠拢”,可以通过位运算提高效率。

二进制的格式虽然对于人阅读理解不是很友好,但是对于机器来说却刚好相反,实际上二进制传输反倒要比明文传输省事多了,因为二进制只有0和1绝对不会造成解析上的歧义,也不会因为编码问题需要额外转译。

二进制帧保留Header+Body传输结构,但是打散了内部的传输格式,把内容拆分为二进制帧的格式,HTTP/2把报文的基本传输单位叫做,而帧分为两个大类 HEADERS(首部)DATA(消息负载),一个消息划分为两类帧传输同时采用二进制编码。

这种做法类似Chunked化整数据的方式,把Header+body等同的帧数据,同时内部通过类型判断进行标记。

这里可以举个简单例子,比如常见的状态码 200,用文本数据传输需要3个字节(二进制:00110010 00110000 00110000),而用HTTP/2的二进制只需要1个字节(10001000)

帧类型后面接着标志位,标志位用于携带一些控制信息,比如下面:

  • END_HEADERS:表示头数据结束标志,相当于 HTTP1.X 里头后的空行“\\r\\n”。
  • END_Stream:表示单方向数据发送结束,后续不会再有数据帧。
  • PRIORITY:表示流的优先级。

最后是31位的流标识符以及1个最高位保留不用的数据,流标识符的最大值是 2^31,大约是 21 亿大小,此标志位的主要作用是标识该 Frame 属于哪个 Stream,乱序传输中根据这部分乱序的帧流标识符号找到相同的Stream Id 进行传输。

RFC 文档定义:
Streams are identified with an unsigned 31-bit integer. Streams initiated by a client MUST use odd-numbered stream identifiers; those initiated by the server MUST use even-numbered stream identifiers.

最后是帧数据,这部分为了提高利用效率使用了HPACK算法压缩的头部和实际数据。

实际上SPDY早期的方案也是使用GZIP压缩,只不过CRIME压缩漏洞攻击之后才专门研究出HPACK算法它防止压缩漏洞攻击。

流与多路复用

核心概念:

  • 流是二进制帧的双向传输序列
  • 一个 HTTP/2 的流就等同于一个 HTTP/1 里的“请求 - 应答”。
  • HTTPP2的流特点
    - 一个TCP复用多个“请求响应”,支持并发传输
    - 流和流之间独立,但是内部通过StreamId保证顺序。
    - 流可以进行请求优先级设置
    - 流ID不允许重复
    - 0号流是用于流量控制的控制帧
    ....

理解多路复用我们需要先了解二进制帧,因为流的概念在HTTP/2中其实是 不存在的,HTTP/2讨论的流是基于二进制帧的数据传输形式的考量。流是二进制帧的双向传输序列

我们这里再复习一遍二进制帧的结构,里面的流标识符就是流ID。

一个 HTTP/2 的流就等同于一个 HTTP/1 里的“请求 - 应答”,而在HTTP1里面,它表示一次报文的“请求响应”,所以HTTP1和HTTP/2在这一点上概念是一样的。

不过按照TCP/IP 的五层传输模型来看,其实TCP的连接概念也是虚拟的,它需要依赖IP运输和MAC地址寻址,但是从功能上来说它们都是实实在在的完成传输动作,所以不需要纠结流虚拟还是不虚拟的概念,我们直接把他当成实际存在的更容易好理解。

HTTP/2 的流主要有下面的特点:

  1. HTTP/2遵循一个TCP上复用多个“请求 - 应答”,意味着一个 HTTP/2 连接上可以同时发出多个流传输数据,并且流可以并发传输实现“多路复用”;
  2. 客户端和服务器都可以创建流,并且互不干扰;
  3. HTTP/2支持服务端推送,流可以从客户端或者服务端出发;
  4. 流内部的帧是有严格顺序的,但是流之间互相独立;
  5. 流可以设置优先级,让服务器优先处理特定资源,比如先传 HTML/CSS,后传图片,优化用户体验;
  6. 流 ID 不能重用,只能顺序递增,客户端发起的 Stream ID 是奇数,服务器端发起的 Stream ID 是偶数;
  7. 在流上发送“RST_STREAM”帧可以随时终止流,取消流的接收或发送;
  8. 第 0 号流比较特殊,它不能关闭,也不能发送数据帧,只能发送控制帧,用于流量控制。

从上面特点那中我们还可以发现一些细节。

默认长连接

比如第一条可以推理出HTTP/2遵循的请求跑在一个TCP连接上,而多个请求的并发传输跑在一个TCP连接的前提是连接有相对长时间占用,也就是说HTTP/2 在一个连接上使用多个流收发数据本身默认就会是长连接,所以永远不需要“Connection”头字段(keepalive 或 close)。

RST_STREAM帧的常见应用是大文件中断重传,在 HTTP/1 里只能断开 TCP 连接重新“三次握手”进行请求重连,这样处理的成本很高,而在 HTTP/2 里就可以简单地发送一个“RST_STREAM”中断流即可进行暂停,此时长连接会继续保持

流标识符不是无限的,如果ID递增到耗尽,此时可以发送控制帧“GOAWAY”,真正关闭 TCP 连接

因为流双向传输,HTTP/2使用了奇数和偶数划分请求来源方向,奇数为客户端发送的帧,而偶数为服务端发送的帧,客户端在一个连接最多发出2^30请求,大约为10亿个。

流状态转化

既然RST_STREAM帧可以改变整个流的传输状态,那么意味着HTTP/2的流是存在状态帧的概念的,翻阅RFC文档果然发现了状态机的图,从下面的可以看到比较复杂。我们重点关注四个状态:

  • idle
  • open
  • half closed
  • closed是不是感觉有点熟悉?没错这和TCP层的连接握手状态其实是有不少相似性的,从这里也可以看出HTTP/2的整个理念是贴近TCP协议层。
         +--------+
                    send PP |        | recv PP
                   ,--------|  idle  |--------.
                  /         |        |         \\
                 v          +--------+          v
          +----------+          |           +----------+
          |          |          | send H /  |          |
   ,------| reserved |          | recv H    | reserved |------.
   |      | (local)  |          |           | (remote) |      |
   |      +----------+          v           +----------+      |
   |          |             +--------+             |          |
   |          |     recv ES |        | send ES     |          |
   |   send H |     ,-------|  open  |-------.     | recv H   |
   |          |    /        |        |        \\    |          |
   |          v   v         +--------+         v   v          |
   |      +----------+          |           +----------+      |
   |      |   half   |          |           |   half   |      |
   |      |  closed  |          | send R /  |  closed  |      |
   |      | (remote) |          | recv R    | (local)  |      |
   |      +----------+          |           +----------+      |
   |           |                |                 |           |
   |           | send ES /      |       recv ES / |           |
   |           | send R /       v        send R / |           |
   |           | recv R     +--------+   recv R   |           |
   | send R /  `----------->|        |<-----------'  send R / |
   | recv R                 | closed |               recv R   |
   `----------------------->|        |<----------------------'
                            +--------+
      send:   endpoint sends this frame
      recv:   endpoint receives this frame
      H:  HEADERS frame (with implied CONTINUATIONs)
      PP: PUSH_PROMISE frame (with implied CONTINUATIONs)
      ES: END_STREAM flag
      R:  RST_STREAM frame

Note that this diagram shows stream state transitions and the frames
and flags that affect those transitions only. In this regard,

CONTINUATION frames do not result in state transitions; they are

effectively part of the HEADERS or PUSH_PROMISE that they follow.

有关流状态转化的细节都在RFC的文档中,链接如下:

https://datatracker.ietf.org/doc/html/rfc7540,上面的图理解起来比较吃力,我们先看一个极简风格的图:

当连接没有开始的时候,所有流都是空闲状态,此时的状态可以理解为“不存在待分配”。客户端发送 HEADERS帧之后,流就会进入"open"状态,此时双端都可以收发数据,发送数据之后客户端发送一个带“END_STREAM”标志位的帧,流就进入了“半关闭”状态。响应数据也需要发送 END_STREAM 帧,表示自己已经接收完所有数据,此时也进入到“半关闭”状态。如果请求流ID耗尽,此时就可以发送一个 GOAWAY 完全断开TCP连接,重新建立TCP握手。

以上就是一个简单的流交互过程。

idel:Sending or receiving a HEADERS frame causes the stream to become "open".
END_STREAM flag causes the stream state to become "half-closed
(local)"; an endpoint receiving an END_STREAM flag causes the

  stream state to become "half-closed (remote)".

并发传输

并发传输是依靠流的多路复用完成的,根据上面的内容我们知道Stream 可以并行在一个TCP连接上,每一个Stream就是一次请求响应,HTTP/2在并发传输中设置了下面几个概念:

  • Stream
  • Message
  • Frame

这三者的关系如下

我们根据结合图以及之前所学,对于这几个概念做出如下定义:

Connection 连接:1 个 TCP 连接,包含 1 个或者多个 stream。所有通信都在一个 TCP 连接上完成,此连接可以承载任意数量的双向数据流。

Stream 数据流:一个双向通信的数据流,包含 1 条或者多条 Message。每个数据流都有一个唯一的标识符和可选的优先级信息,用于承载双向消息。

Message 消息:对应 HTTP/1.1 中的请求 request 或者响应 response,包含 1 条或者多条 Frame。

Frame 数据帧:最小通信单位,以二进制压缩格式存放内容。来自不同数据流的帧可以交错发送,然后再根据每个帧头的数据流标识符重新组装。

HTTP1.1 由 Start Line + header + body 组成,HTTP2转变为HEADER frame + 若干个 DATA frame 组成。

在HTTP2中,消息允许客户端或者服务器以Stream为基础进行乱序发送,内部被拆分为独立的帧。

注意Value字段是动态变化的,Value设置之前都需要进行哈夫曼编码,编码之后通常具备50%左右的字节占用减少,比如高亮部分是 server 头部字段,只用了 8 个字节来表示 server 头部数据。

RFC中规定,如果头部字段属于静态表范围并且 如果Value 是变化的,那么它的 HTTP/2 头部前 2 位固定为 01

通过抓包了解server在HTTP的格式:

server: nghttpx\\r\\n
哈夫曼编码之后:
server: 01110110

算上冒号空格和末尾的\\r\\n,共占用了 17 字节,而使用了静态表和 Huffman 编码,可以将它压缩成 8 字节,压缩率大概 47 %

上面的 server的值是如何定义的,首先通过index找到 server字段的序列号为54,二进制为110110,同时它的Value是变化的,所以是01开头,最后组成01110110

接着是Value部分,根据上文RFC哈夫曼编码的规则,首个比特位是用来标记是否哈夫曼编码的,所以跳过字节首位,后面的7位才是真正用于标识Value的长度,10000110,它的首位比特位为 1 就代表 Value 字符串是经过 Huffman 编码的,经过 Huffman 编码的 Value 长度为 6。

整个进化结果就是,字符串 nghttpx 转为二进制之后,然后经过 Huffman 编码后压缩成了 6 个字节。 哈夫曼的核心思想就是把高频出现的“单词”用尽可能最短的编码进行存储,比如 nghttpx 对应的哈夫曼编码表如下:

一共是六个字节的数据,从二进制通过查表的结果如下:

server 头部的二进制数据对应的静态头部格式如下:

注意\\r\\n是不需要二进制编码的。01 表示变化的静态表字段。

动态表

静态表包含了固定字段但是值不一定固定的表,而动态表则用存储静态表中不存在的字段,动态表从索引号62开始,编码的时候会随时进行更新。

比如第一次发送user-agent字段,值经过哈夫曼编码之后传输给接收方,双方共同存储值到各自的动态表上,下一次如果需要同样的user-agent字段只需要发送序列号index即可,因为双方都把值存储在各自对应的index索引当中。

所以哪怕字段越来越多,只要经过了哈夫曼编码存储以及通过索引号能找到对应的参数,就可以有效减少重复数据的传输。

哈夫曼编码

哈夫曼编码是一种用于无损数据压缩熵编码(权编码)算法。由美国计算机科学家大卫·霍夫曼(David Albert Huffman)在1952年发明。 霍夫曼在1952年提出了最优二叉树的构造方法,也就是构造最优二元前缀编码的方法,所以最优二叉树也别叫做霍夫曼树,对应最优二元前缀码也叫做霍夫曼编码。

哈夫曼编码对于初学者来说不是特别好理解,这部分内容放到了[哈夫曼编码]中进行讨论。

概念不好理解,初学建议多去找找视频教程对比学习

Header 压缩问题

这部分实际上指的是HTTP3 对于HTTPS的Header压缩优化,既然是优化,我们反向思考就可以知道问题了,主要是下面三点:

  • 请求接收端的处理能力有限,Header 压缩不能设置过于极限,缓存表如果占用超过一定的占比就会释放掉整个连接重新请求。(空间换时间不可避问题)
  • 静态表容量不够,HTTP3 升级到91个。
  • HTTP/2的动态表存在时序性问题,编码重传会造成网络拥堵。

缓存表限制:浏览器的内存以及客户端以及服务端的内存都是有限的,尤其是动态表的不确定因素很大,HTTP标准设计要求防止动态表过度膨胀占用内存导致客户端崩溃,并且在超过一定长度过后会自动释放HTTP/2请求。

保守设置:压缩表的设置有点过于保守了,所以HTTP3 对于这个表进行进一步扩展。

时序性问题:时序性问题是在传输的时候如果出现丢包,此时一端的动态表做了改动但是另一端是没改变的,同样需要把编码重传,这也意味着整个请求都会阻塞掉。

权重值越小,优先级越高

HTTP/2 中的流依赖项是通过引用另一个流的唯一标识符作为其父级来进行声明的。如果没有标识,则认为是root stream,声明流依赖项设计表示应在其依赖项之前尽可能为父级分配资源,举例来说就是在上面的响应中,先交付并且处理D,然后才进行C的处理。

共享同一父级的流(换句话说,同级流)应按其权重的比例分配资源。例如如果流 A 的权重为 12,同级 B 的权重为 4,每个流应接收资源比例计算如下:

  1. 首先把所有的权重值相加, 4+12 = 16。
  2. 计算A和B在权重值中所占据的比例:4 / 16,12 / 16。
  3. 按照比例计算,流A 获得3/4的可用资源,流B获得1/4的可用资源。
  4. D依赖root stream,而C依赖D,所以D可以获得全部的资源分配,然后再轮到C分配。
  5. 流D先于C获得资源的全部分配,C应在A和B之前获得资源的全部分配,剩下的再分配给A和B,同时流 B 应接收分配给流 A 的资源之后剩下的 1 / 4。
  6. 按照同样的道理,流D应在E和C之前获得资源的全部分配,E和C应该在A和B之前获得相等的分配,A 和 B 应根据其权重获得比例分配,流B接收分配给A 3/4 的最后1/4。

流依赖和权重值简洁易懂的实现一种权重分配的表达语言,通过这些表达语言来强化浏览器性能,比如用户看的见的CSS、JS脚本、HTML页面优先暂时,第一时间告知网站在积极响应而提高用户体验。

HTTP/2 协议允许客户端随时更新这些首选项从而进一步优化浏览器,换句话说我们可以随时更改依赖关系并重新分配权重,以响应用户交互和其他信号。

注意⚠️:流依赖关系和权重表示传输首选项而不是强制要求,因此实际上哪怕指定了请求优先级也并不能不保证一定按照特定的处理或传输顺序。也就是说客户端不能强制使用流优先级要求服务器按特定顺序处理流。undefined所以可以认为优先级的设置更像是“期望”,双端期望对方按照自己想要的结果处理。比如期望浏览器获取较高优先级的资源之前,阻止服务器在较低优先级的资源上进行处理。

小结

  • 请求优先级关键设计来源于一个有趣的“语言模型”:
    • 1 和 256 之间的整数权重
    • 树状流和流之间依赖关系
  • 流依赖关系和权重表示传输首选项而不是强制要求
  • 请求优先级不能规定行为,而是期望

流量控制

HTTP/2的流量控制是依靠帧结构实现的,通过关键字段WINDOW_UPDATE帧来提供流量控制,根据结构体定义,这个帧固定为4个字节的长度:

WINDOW_UPDATE Frame {
  Length (24) = 0x04,
  Type (8) = 0x08,

  Unused Flags (8),

  Reserved (1),
  Stream Identifier (31),

  Reserved (1),
  Window Size Increment (31),
}

对于流量控制,存在下面几个显著特征:

  • 流控制仅适用于被识别为受流量控制的帧(DATA 帧),同时流量的控制存在方向概念,由数据的双端负责流量控制,可以设置每一个流窗口的大小。
  • 流量控制需要受到各种代理服务器限制,并不完全靠谱,比如如果IP的一跳中存在代理,则代理和双端都有流控,所以特别注意这并非端到端的控制;
  • 基于信用基础公布每个流在每个连接上接收了多少字节,WINDOW_UPDATE 框架没有定义任何标志;换句话说只定义了几个基本的帧字段格式定义,怎么发送接收和控制完全由实现方决定,保证流控的自由度。
  • WINDOW_UPDATE 可以对已设置了 END_STREAM 标志的帧进行发送,表示接收方这时候有可能进入了半关闭或者已经关闭的状态接收到WINDOW_UPDATE帧,但是接收者不能视作错误对待;
  • 接收者必须将接收到流控制窗口增量为 0 的 WINDOW_UPDATE帧视为PROTOCOL_ERROR类型的流错误 ;
  • 对于连接与所有新开启的流而言,流控窗口大小默认都是 65535,且最大值为 2^32;
  • 流控无法禁用
  • 流控既可以作用于 stream 也可以作用于 connection。

了解流量控制的注意事项,我们看看它是如何实现的?

流量控制窗口 (Flow Control Window)

每个发送端会存在一个叫做流量窗口的东西,里面简单保存了整数值,标识发送端允许传输的,当流量窗口没有可用空间时,可以发送带有END_STREAM 的帧标记。

但是发送端的流量窗口没有多大意义,这有点类似把井水装到一个桶里面,主要的限制不是井里有多少水,而是看桶可以装多少水,所以为了确保网络正常传输,发送端传输长度不能超过超出接收端广播的流量控制窗口大小的可用空间长度。

WINDOW_UPDATE 帧

前面多次提到的 WINDOW_UPDATE帧有什么用?主要作用是给接收端告知自己的接收能力,如果提供这个帧,那么发送方不管有多强能力,都需要按照提供的长度限制进行数据发送。

WINDOW_UPDATE帧要么单独作用于 stream,要么单独作用于 connection(streamid 为 0 时,表示作用于 connection,无接收能力)

我们根据流量窗口和WINDOW_UPDATE帧了解基本算法流程如下:

  1. 发送方提供流量窗口初始值,初始值是SETTING 帧,这个帧的参数设置十分关键,比如 SETTINGS_INITIAL_WINDOW_SIZE表示窗口初始大小,默认初始值和最大值均为 65535

SETTINGS_INITIAL_WINDOW_SIZE (0x4): Indicates the sender's initial
window size (in octets) for stream-level flow control. The

  initial value is 2^16-1 (65,535) octets.
  1. 发送端每发送一个DATA帧,就把window流量窗口的值递减,递减量为这个帧的大小,如果流量窗口大小小于DATA帧,则必须对于流进行拆分,直到小于windows流量窗口为止,而流量窗口递减到0的时候,不能发送任何帧。
  2. 接收端通过 WINDOW_UPDATE 帧,告知发送方自己的负载能力。

SETTING 帧

本节最后我们再补充一下SETTING 帧的选项含义:

  • SETTINGS_HEADER_TABLE_SIZE:HPACK(header压缩算法) header表的最大长度,默认值 4096
  • SETTINGS_ENABLE_PUSH:客户端发向服务端的配置,若设置为 true,客户端将允许服务端推送响应,默认值 true
  • SETTINGS_MAX_CONCURRENT_STREAMS:同时打开的 stream 最大数量,通常意味着同一时刻能够同时响应的请求数量,默认无限
  • SETTINGS_INITIAL_WINDOW_SIZE:流控的初始窗口大小,默认值 65535
  • SETTINGS_MAX_FRAME_SIZE:对端能够接收帧的最大长度,默认值16384
  • SETTINGS_MAX_HEADER_LIST_SIZE:对端能够接收的 header 列表最大长度,默认不限制

题外话:httpcore5 的 BUG

httpcore5 过去的版本存在流控的BUG,但是这个问题很快被发现并且被修复。

因为涉及流控触发BUG的概率还是挺大的,也是比较严重的BUG,BUG修复可以看这个 COMMIT,想看具体分析可以看参考文章的第一篇。下面为个人阅读文章之后分析思路。

我们以 URL https://www.sysgeek.cn/ 为例,通过在本地做代码 debug 发现,最终抛异常的原因在于接收到 WINDOW_UPDATE 帧后,更新后窗口大小值大于 2^32 - 1导致抛异常:

首先根据 commit log,修复者自己也进行了说明。

The connection flow-control window can only be changed using
> WINDOW_UPDATE frames.

我们接着对照 RFC 的文档定义:

意思是说connection 窗口大小仅在接收到 WINDOW_UPDATE 后才可能修改这个规则被违背的。

把代码扒出来看一下改了什么:

private void applyRemoteSettings(final H2Config config) throws H2ConnectionException {
	
	remoteConfig = config;
	
	hPackEncoder.setMaxTableSize(remoteConfig.getHeaderTableSize());
	
	final int delta = remoteConfig.getInitialWindowSize() - initOutputWinSize;
	
	initOutputWinSize = remoteConfig.getInitialWindowSize();
	
	  
	
	if (delta != 0) {
		// 关键BUG修复
		updateOutputWindow(0, connOutputWindow, delta);
	
	if (!streamMap.isEmpty()) {
	
		for (final Iterator<Map.Entry<Integer, H2Stream>> it = streamMap.entrySet().iterator(); it.hasNext(); ) {
			
			final Map.Entry<Integer, H2Stream> entry = it.next();
			
			final H2Stream stream = entry.getValue();
			
			try {
			
			updateOutputWindow(stream.getId(), stream.getOutputWindow(), delta);
			
			} catch (final ArithmeticException ex) {
			
			throw new H2ConnectionException(H2Error.FLOW_CONTROL_ERROR, ex.getMessage());
		
		}
	
	}
	
	}
	
	}

}

delta 是对方告知的 WINDOW_UPDATE 大小,问题出在接收 SETTINGS 指令之后,初始化的窗口大小被修改了,原本的6555被改成更大的值,这个值超过了流量窗口的默认值和最大值的上限,但是流量窗口的大小必须是WINDOW_UPDATE帧传输之后才允许更改,发送方擅自修改并且发送了超过接收方能力的流量,被检查出异常流量而在代码中抛出异常。

这个很好理解,就好像井水不管桶有多大,就一个劲的往里面灌水,这肯定是有问题的。

服务器推送

概括:

  • 管道化改良
  • 偶数帧数为起始
  • 依靠PUSH_PROMISE帧传输头部信息
  • 通过帧中的 Promised Stream ID 字段告知偶数号

服务器推送的RFC定义:RFC 7540: Hypertext Transfer Protocol Version 2 (HTTP/2) (rfc-editor.org)

服务器推送是为了弥补HTTP这个半双工协议的短板,虽然HTTP1.X 尝试使用管道流实现服务端推送,但是管道流存在各种缺陷所以HTTP1.X并没有实现服务端推送的功能。

注意在上面提到的二进制帧数据传输中中,客户端发起的请求必须使用的是奇数号 Stream,服务器主动的推送请求使用的是偶数号 Stream,所以如果是服务端推送通常是从偶数开始。

服务端推送资源需要依靠PUSH_PROMISE帧传输头部信息,并且需要通过帧中的 Promised Stream ID 字段告知客户端自己要发送的偶数号。

需要服务端推送存在诸多限制,从整体上看服务端推送的话语权基本是在客户端这边,下面简单列举几点:

  • 客户端可以设置 SETTINGS_MAX_CONCURRENT_STREAMS=0 或者重置PUSH_PROMISE拒绝服务端推送。
  • 客户端可以通过SETTINGS_MAX_CONCURRENT_STREAMS设置服务端推送的响应。
  • PUSH_PROMISE帧只能通过服务端发起,使用客户端推送是“不合法“的,服务端有权拒绝。

补充

连接前言

这个连接前言算是比较偏门的点,也常常容易被忽略。如果能看懂下面的内容,那么基本就知道怎么会回事了。

   In HTTP/2, each endpoint is required to send a connection preface as
   a final confirmation of the protocol in use and to establish the
   initial settings for the HTTP/2 connection.  The client and server
   each send a different connection preface.

   The client connection preface starts with a sequence of 24 octets,
   which in hex notation is:

     0x505249202a20485454502f322e300d0a0d0a534d0d0a0d0a

   **That is, the connection preface starts with the string "PRI *
   HTTP/2.0\\r\\n\\r\\nSM\\r\\n\\r\\n").**  This sequence MUST be followed by a
   SETTINGS frame ([Section 6.5](https://datatracker.ietf.org/doc/html/rfc7540#section-6.5)), which MAY be empty.  The client sends
   the client connection preface immediately upon receipt of a 101
   (Switching Protocols) response (indicating a successful upgrade) or
   as the first application data octets of a TLS connection.  If
   starting an HTTP/2 connection with prior knowledge of server support
   for the protocol, the client connection preface is sent upon
   connection establishment.

连接前言的关键点如下:

  • “连接前言”是标准的 HTTP/1 请求报文,使用纯文本的 ASCII 码格式,请求方法是特别注册的一个关键字“PRI”,全文只有 24 个字节。
  • 如果客户端在建立连接的时候使用 PRI * HTTP/2.0\\r\\n\\r\\nSM\\r\\n\\r\\n,并且通过 SETTINGS 帧告知服务端自己期望HTTPS2 连接,服务端就知道客户端需要的是TLS的HTTP/2连接。

为什么是这样的规则,以及为什么是传输这样一串奇怪的字符无需纠结,这是HTTP/2标准制定者指定的规矩,所以就不要问“为什么会是这样”了。

其实把这一串咒语拼起来还是有含义的,PRISM,2013年斯诺登的“棱角计划”,这算是在致敬?

流和管道化关系

HTTP/2的流是对于HTTP1.X的管道化的完善以及改进,所以在流中可以看到不少管道化的概念。而HTTP/2 要比管道化更加完善合理,所以管道化的概念在HTTP/2之后就被流取代而消失了。

请求头字段约束

因为HTTP1.X对于头字段写法很随意,所以HTTP/2设置所有的头字段必须首字母小写。

 Just as in HTTP/1.x, header field names are strings of ASCII
   characters that are compared in a case-insensitive fashion.  However,
   header field names MUST be converted to lowercase prior to their
   encoding in HTTP/2

就像在 HTTP/1.x 中一样,标头字段名称是 ASCII 字符串
    以不区分大小写的方式比较的字符。 然而,
    标头字段名称必须在其之前转换为小写
    HTTP/2 中的编码

总结

我们按照重点排序,来从整体上看一下HTTP2的知识点,为此我总结了几个关键字:

重塑:不是指完全重造,而是借用HTTP协议的基本架构,从内部进行重新调整。

兼容:HTTP协议背负巨大的历史包袱,所有的改动如果无法向后兼容,那么就是失败的升级,也不会受到广泛认可。所以HTTP2整体结构沿用HTTP1.X,加入连接前言这种和TLS握手类似的“咒语”完成新协议的启用。

状态:Header压缩的HACK技术加入之后,HTTP似乎不再像是以前那样的无状态协议,它的动态表和静态表都是实际存在的,每个HTTP2的连接都会出现状态维护,所以虽然本身外部实现不需要关注这些细节,实际上HTTP2 内部确实加了状态这个概念。

贴合TCP:HTTP2的很多细节不难看出是为了更好的和TCP协调,比如二进制数据。

管道化延伸:管道化在HTTP1.X中非常鸡肋,而HTTP2则把管道化的理念改进为流的概念进行数据传输,并且依靠流实现并发传输。

写到最后

来来回回改了很多次,自认为把HTTP2主要的知识点普及了,更多细节需要深入RFC文档,不过不是专攻网络编程方向的个人也就点到为止了。

参考文章

本站文章资源均来源自网络,除非特别声明,否则均不代表站方观点,并仅供查阅,不作为任何参考依据!
如有侵权请及时跟我们联系,本站将及时删除!
如遇版权问题,请查看 本站版权声明
THE END
分享
二维码
海报
HTTP - HTTP/2 知识点
在《图解HTTP》的读书笔记[《图解HTTP》- HTTP协议历史发展(重点)]当中介绍了一部分关于HTTP/2的内容,但是内容比较简短没有过多深入,本文对于H...
<<上一篇
下一篇>>