这是一篇理论性较强的文章
1. HTTP/1的不足与面临的问题
此部分内容来自:https://segmentfault.com/a/1190000013519925
1.1 HTTP/1概述
1.2 队头阻塞
浏览器很少只从一个域名获取一份资源。大多数时候,它希望能同时获取许多资源。设想这样一个网站,它把所有图片放在单个特定域名下。HTTP/1 并未提供机制来同时请求这些资源。如果仅仅使用一个连接,它需要发起请求、等待响应,之后才能发起下一个请求。这显然会在加载页面时造成较大的延迟,降低了用户体验。
h1 有个特性叫 管道化(pipelining),允许一次发送一组连续的请求,而不用等待应答返回。这样可以避免连接延迟。但是该特性只能按照发送顺序依次接收响应。而且,管道化备受互操作性和部署的各种问题的困扰,基本没有实用价值。
在请求应答过程中,如果出现任何状况,剩下所有的工作都会被阻塞在那次请求应答之后。这就是队头阻塞
(Head-of-line blocking或缩写为HOL blocking),它会阻碍网络传输和 Web 页面渲染,直至失去响应。
为了防止这种问题,现代浏览器会针对单个域名开启 6 个连接,通过各个连接分别发送请求。它实现了某种程度上的并行,但是每个连接仍会受到 队头阻塞 的影响。另外,这也没有高效利用有限的设备资源。
1.3 TCP复用
传输控制协议(TCP) 的设计思路是:对假设情况很保守,并能够公平对待同一网络的不同流量的应用。它的避免拥塞机制被设计成即使在最差的网络状况下仍能起作用,并且如果有需求冲突也保证相对公平。这是它取得成功的原因之一。
它的成功并不是因为传输数据最快,而是因为它是最可靠的协议之一,涉及的核心概念就是 拥塞窗口(congestion window) 。拥塞窗口是指,在接收方确认数据包之前,发送方可以发出的 TCP 包的数量。 例如,如果拥塞窗口指定为 1,那么发送方发出 1 个数据包之后,只有接收方确认了那个包,才能发送下一个。
一般来讲,每次发送一个数据包并不是非常低效。TCP 有个概念叫 慢启动(Slow Start), 它用来探索当前连接对应拥塞窗口的合适大小。慢启动的设计目标是为了让新连接搞清楚当前网络状况,避免给已经拥堵的网络继续添乱。它允许发送者在收到每个确认回复后额外发送 1 个未确认包。这意味着新连接在收到 1 个确认回复之后,可以发送 2 个数据包; 在收到 2 个确认回复之后,可以发 4 个;以此类推。这种几何级数增长很快就会到达协议规定的发包数上限,这时候连接将进入拥塞避免阶段
这种机制需要几次往返数据请求才能得知最佳拥塞窗口大小。但在解决性能问题时,就这 区区几次数据往返也是非常宝贵的时间(成本)。现代操作系统一般会取 4~10 个数据包作为初始拥塞窗口大小。如果你把一个数据包设置为最大值下限 1460 字节(也就是 最大有效负载),那么只能先发送 5840 字节(假定拥塞窗口为 4),然后就需要等待接收确认回复。
如今的 Web 页面平均大小约 2MB,包括 HTML 和所有依赖的资源。在理想情况下, 这需要大约 9 次往返请求来传输完整个页面。除此之外,浏览器一般会针对同一个域名开启 6 个并发连接,这意味着拥塞窗口波动也会并行发生 6 次。TCP 协议保证那些连接都能正常工作, 但是不能保证它们的性能是最优的。
1.4 消息首部
虽然 h1 提供了压缩被请求内容的机制,但是消息首部却无法压缩。消息首部可不能忽略, 尽管它比响应资源小很多,但它可能占据请求的绝大部分(有时候可能是全部)。如果算 上 cookie,有个几千字节就很正常了。
据 HTTP 历史存档记录,2016 年末,请求首部一般集中在 460 字节左右。对于包含 140 个 资源的普通Web 页面,意味着它在发起的所有请求中大约占 63KB。
想想之前关于 TCP 拥塞窗口管理的讨论,发送该页面相关的所有请求可能需要 3~4 轮往返,因此网络延迟的损耗会被迅速放大。此外,上行带宽通常会受到网络限制,尤其是在移动网络环境中,于是拥塞窗口机制根本来不及起作用,导致更多的请求和响应。
消息首部压缩的缺失也容易导致客户端到达带宽上限,对于低带宽或高拥堵的链路尤其如此。体育馆效应(Stadium Effect) 就是一个经典例子。如果成千上万人同一时间出现在同一地点(例如重大体育赛事),会迅速耗尽无线蜂窝网络带宽。这时候,如果能压缩请求首部,把请求变得更小,就能够缓解带宽压力,降低系统的总负载。
1.5 优先级设置受限
如果浏览器针对指定域名开启了多个 socket(每个都会受队头阻塞问题的困扰),开始请求资源,这时候浏览器能指定优先级的方式是有限的:要么发起请求,要么不发起。
然而 Web 页面上某些资源会比另一些更重要,这必然会加重资源的 排队效应。这是因为浏览器为了先请求优先级高的资源,会推迟请求其他资源。
但是优先级高的资源获取之后,在处理的过程中,浏览器并不会发起新的资源请求,所以服务器无法利用这段时间发送优先级低的资源,总的页面下载时间因此延长了。还会出现这样的情况:一个高优先级资源被浏览器发现,但是受制于浏览器处理的方式,它被排在了一个正在获取的低优先级资源之后。
以下内容来自:RFC7540
2. HTTP/2概述
HTTP/2 为 HTTP 语义提供了优化的传输。HTTP/2 支持 HTTP/1.1 的所有核心功能,但旨在通过多种方式提高效率。
HTTP/2 中的基本协议单元是帧。每种帧类型都有不同的用途。例如,HEADERS和DATA帧构成了 HTTP 请求和响应的基础;其他帧类型如SETTINGS、WINDOW_UPDATE和PUSH_PROMISE用于支持其他 HTTP/2 功能。
请求的复用是通过让每个 HTTP 请求/响应交换与其自己的流相关联来实现的。流在很大程度上彼此独立,因此阻塞或停滞的请求或响应不会阻止其他流的进展。
流控制和优先级确保可以有效地使用多路复用流。流量控制有助于确保仅传输接收器可以使用的数据。优先级确保可以将有限的资源首先用于最重要的流。
HTTP/2 添加了一种新的交互模式,服务器可以通过这种模式将响应推送到客户端。服务器推送允许服务器推测性地将数据发送到服务器预期客户端需要的客户端,权衡一些网络使用与潜在的延迟增益。服务器通过合成一个请求来做到这一点,它作为一个PUSH_PROMISE帧发送。然后,服务器能够在单独的流上发送对合成请求的响应。
由于连接中使用的 HTTP 标头字段可能包含大量冗余数据,因此包含它们的帧会被压缩。这对常见情况下的请求大小具有特别有利的影响,允许将许多请求压缩为一个数据包。
3. HTTP/2帧
3.1 帧格式
所有帧都以固定的 9 个八位字节标头开始,后跟可变长度的有效载荷。
+-----------------------------------------------+
| 长度 (24) |
+---------------+---------------+---------------+
| 类型 (8) | 旗帜 (8) |
+-+-------------+---------------+-------------------------------+
|R| 流标识符 (31) |
+=+=============================================================+
| 帧有效载荷 (0...) ...
+---------------------------------------------------------------+
帧头的字段定义为:
长度: 以无符号 24 位整数表示的帧有效载荷的长度。除非接收方为SETTINGS_MAX_FRAME_SIZE设置了更大的值,否则不得发送大于 $2^14$ (16,384) 的值。 该值中不包括帧头的 9 个八位字节。
类型: 帧的 8 位类型。帧类型决定了帧的格式和语义。实现必须忽略并丢弃任何类型未知的帧。
标志: 为特定于帧类型的布尔标志保留的 8 位字段。
标志被分配特定于指示的帧类型的语义。对于特定帧类型没有定义语义的标志必须被忽略并且在发送时必须保持未设置 (0x0)。
回复: 保留的 1 位字段。该位的语义未定义,并且该位在发送时必须保持未设置 (0x0),在接收时必须被忽略。
流标识符: 表示为无符号 31 位整数的流标识符(参见第 5.1.1 节)。值 0x0 保留用于与整个连接相关联的帧,而不是单个流。
帧有效载荷的结构和内容完全取决于帧类型
3.2 帧大小
帧有效载荷的大小受接收器在SETTINGS_MAX_FRAME_SIZE设置中通告的最大大小的限制。此设置可以具有 $2^14$ (16,384) 和 $2^24 -1$ (16,777,215) 个八位字节之间的任何值,包括两个字节。
所有实现都必须能够接收和最少处理长达 $2^14$ 个八位字节的帧,加上 9 个八位字节的帧头。描述帧大小时不包括帧头的大小。
注意:某些帧类型,例如 PING,对允许的有效载荷数据量施加了额外的限制。
如果帧超过SETTINGS_MAX_FRAME_SIZE 中定义的大小,超过为帧类型定义的任何限制,或者太小而无法包含强制性帧数据,则端点必须发送错误代码FRAME_SIZE_ERROR。帧中可能改变整个连接状态的帧大小错误必须被视为连接错误;这包括任何带有标题块(即HEADERS、PUSH_PROMISE和CONTINUATION)、SETTINGS 的帧,以及任何流标识符为 0 的帧。
端点没有义务使用帧中的所有可用空间。可以通过使用小于允许的最大尺寸的帧来提高响应能力。发送大帧可能会导致发送时间敏感帧(例如RST_STREAM、WINDOW_UPDATE或PRIORITY)的延迟,如果被大帧的传输阻塞,则可能会影响性能
3.3 头压缩和解压
就像在 HTTP/1 中一样,HTTP/2 中的标头字段是一个具有一个或多个关联值的名称。标头字段用于 HTTP 请求和响应消息以及服务器推送操作。
标题列表是零个或多个标题字段的集合。当通过连接传输时,标头列表使用HTTP 标头压缩 [COMPRESSION]序列化为标头块。然后将序列化的头块分成一个或多个八位字节序列,称为头块片段,并在 HEADERS、PUSH_PROMISE或 CONTINUATION帧的有效载荷内传输。
该Cookie头字段 [COOKIE]是由HTTP映射特殊处理。
接收端点通过连接其片段来重新组装头块,然后解压缩块以重建头列表。
一个完整的头块包括:
- 设置了 END_HEADERS 标志的单个HEADERS或PUSH_PROMISE帧,或
- 清除了 END_HEADERS 标志的HEADERS或PUSH_PROMISE帧和一个或多个CONTINUATION帧,其中最后一个CONTINUATION帧设置了 END_HEADERS 标志。
标头压缩是有状态的。整个连接使用一个压缩上下文和一个解压缩上下文。头块中的解码错误必须被视为COMPRESSION_ERROR类型的连接错误。
每个标题块都作为一个离散单元进行处理。头块必须作为连续的帧序列传输,没有任何其他类型的交错帧或来自任何其他流。HEADERS或CONTINUATION帧序列中的最后一帧设置了 END_HEADERS 标志。PUSH_PROMISE或CONTINUATION帧序列中的最后一帧设置了 END_HEADERS 标志。这允许标题块在逻辑上等同于单个帧。
标头块片段只能作为HEADERS、PUSH_PROMISE或CONTINUATION帧的有效载荷发送,因为这些帧携带的数据可以修改接收器维护的压缩上下文。接收HEADERS、PUSH_PROMISE或CONTINUATION帧的端点需要重新组装头块并执行解压缩,即使这些帧将被丢弃。如果接收方没有解压缩头块,则接收方必须以COMPRESSION_ERROR类型的连接错误终止连接。
4. 流和复用
“流”是在 HTTP/2 连接内在客户端和服务器之间交换的独立的双向帧序列。流有几个重要的特征:
- 单个 HTTP/2 连接可以包含多个并发打开的流,其中任一端点交错来自多个流的帧。
- 流可以单方面建立和使用,也可以由客户端或服务器共享。
- 流可以被任一端点关闭。
- 在流上发送帧的顺序很重要。接收者按照接收到的顺序处理帧。特别是,HEADERS和DATA帧的顺序在语义上很重要。
- 流由一个整数标识。流标识符由发起流的端点分配给流。
4.1 流状态
流的生命周期如图所示:
+--------+
送PP | | 接收PP
,--------| 闲置|--------。
/ | | \
v + -------- + v
+----------+ | +----------+
| | | 发送 H / | |
,------| 保留 | | 接收 H | 保留|------。
| | (本地) | | | (远程) | |
| + ---------- + v + ---------- + |
| | +--------+ | |
| | 接收 EN | | 发送 EN | |
| 送H | ,-------| 打开|-------。| 接收 H |
| | / | | \ | |
| vv + -------- + vv |
| +----------+ | +----------+ |
| | 一半| | | 一半| |
| | 关闭 | | 发送 R / | 关闭 | |
| | (远程) | | 接收 R | (本地) | |
| +----------+ | +----------+ |
| | | | |
| | 发送 ES / | 接收 EN / | |
| | 发送 R / v 发送 R / | |
| | 接收 R +--------+ 接收 R | |
| 发送 R /`----------->| |<-----------' 发送 R / |
| 接收 R | 关闭 | 接收 R |
`----------------------->| |<----------------------'
+--------+
发送:端点发送此帧
recv:端点接收此帧
H:HEADERS 框架(带有隐含的 CONTINUATION)
PP:PUSH_PROMISE 框架(带有隐含的 CONTINUATION)
ES:END_STREAM 标志
R:RST_STREAM 帧
请注意,此图仅显示了流状态转换以及影响这些转换的帧和标志。在这方面,CONTINUATION帧不会导致状态转换;它们实际上是它们遵循的HEADERS或PUSH_PROMISE 的一部分。出于状态转换的目的,END_STREAM 标志作为单独的事件处理到承载它的帧;设置了 END_STREAM 标志的HEADERS帧会导致两个状态转换。
两个端点都对流的状态有主观看法,当帧在传输时可能会有所不同。端点不协调流的创建;它们由任一端点单方面创建。状态不匹配的负面影响仅限于发送RST_STREAM后的“关闭”状态,在关闭后的一段时间内可能会收到帧。
流具有以下状态:
- idle
- reserved(local)
- reserved(remote)
- open
- half-closed(local)
- half-open(remote)
- closed
4.1.1 流标识符
流由一个无符号的 31 位整数标识。客户端发起的流必须使用奇数流标识符;那些由服务器发起的必须使用偶数流标识符。零 (0x0) 的流标识符用于连接控制消息;零的流标识符不能用于建立新的流。
升级到 HTTP/2(参见第 3.2 节)的HTTP/1.1 请求将使用流标识符 1 (0x1) 进行响应。升级完成后,流 0x1 对客户端来说是“半关闭(本地)”。因此,从 HTTP/1.1 升级的客户端不能选择流 0x1 作为新的流标识符。
新建立的流的标识符在数字上必须大于发起端点已打开或保留的所有流。这控制使用HEADERS帧打开的流和使用PUSH_PROMISE保留的流。接收到意外流标识符的端点必须响应类型为PROTOCOL_ERROR的连接错误(第 5.4.1 节)。
新流标识符的第一次使用会隐式关闭所有处于“空闲”状态的流,这些流可能已由具有较低值的流标识符的对等方启动。例如,如果客户端在流 7 上发送HEADERS帧,但从未在流 5 上发送过帧,则当发送或接收流 7 的第一个帧时,流 5 将转换为“关闭”状态。
流标识符不能重复使用。长期连接可能导致端点耗尽流标识符的可用范围。无法建立新流标识符的客户端可以为新流建立新连接。无法建立新流标识符的服务器可以发送GOAWAY帧,以便客户端被迫为新流打开新连接。
4.1.2 流并发
对等方可以使用SETTINGS帧内的SETTINGS_MAX_CONCURRENT_STREAMS参数(参见第 6.5.2 节)限制并发活动流的数量。最大并发流设置特定于每个端点,并且仅适用于接收设置的对等方。也就是说,客户端指定服务器可以启动的最大并发流数,服务器指定客户端可以启动的最大并发流数。
处于“打开”状态或处于“半关闭”状态之一的流计入允许端点打开的最大流数。处于这三种状态中的任何一种状态的流都计入SETTINGS_MAX_CONCURRENT_STREAMS设置中公布的限制。处于任一“保留”状态的流不计入流限制。
端点不得超过其对等方设置的限制。收到HEADERS帧导致超出其通告的并发流限制的端点必须将此视为PROTOCOL_ERROR或REFUSED_STREAM类型的流错误(第 5.4.2 节)。错误代码的选择决定了端点是否希望启用自动重试(详细信息请参见第 8.1.4 节)。
希望将SETTINGS_MAX_CONCURRENT_STREAMS的值减少到低于当前打开流数的值的端点可以关闭超过新值的流或允许流完成
4.2 流量控制
使用流进行多路复用会引入对 TCP 连接使用的争用,从而导致流被阻塞。流量控制方案确保同一连接上的流不会相互破坏性地干扰。流控制用于单个流和整个连接。
HTTP/2 通过使用 WINDOW_UPDATE 帧提供流量控制
4.2.1 流量控制原理
HTTP/2 流控制旨在允许使用各种流控制算法而无需更改协议。HTTP/2 中的流量控制具有以下特点:
- 流控制特定于连接。两种类型的流量控制都在单跳的端点之间,而不是在整个端到端路径上。
- 流量控制基于WINDOW_UPDATE帧。接收器通告他们准备在一个流上和整个连接上接收多少个八位字节。这是一个基于信用的计划。
- 流量控制是定向的,由接收器提供整体控制。接收者可以选择为每个流和整个连接设置它所需的任何窗口大小。发送方必须遵守接收方施加的流量控制限制。客户端、服务器和中介都独立地将其流量控制窗口作为接收方进行通告,并在发送时遵守其对等方设置的流量控制限制。
- 对于新流和整个连接,流控制窗口的初始值是 65,535 个八位字节。
- 帧类型决定流控制是否适用于帧。在本文档中指定的帧中,只有DATA帧受到流控;所有其他帧类型不占用广告流控制窗口中的空间。这确保重要的控制帧不会被流量控制阻塞。
- 无法禁用流量控制。
- HTTP/2 只定义了WINDOW_UPDATE帧的格式和语义(第 6.9 节)。本文档没有规定接收方如何决定何时发送该帧或发送的值,也没有规定发送方选择如何发送数据包。实现能够选择任何适合他们需要的算法。
实现还负责管理如何根据优先级发送请求和响应,选择如何避免请求的队头阻塞,以及管理新流的创建。这些的算法选择可以与任何流量控制算法交互
4.2.2 流优先级
客户端可以通过在打开流的 HEADERS 帧中包含优先级信息来为新流分配优先级。在任何其他时间,PRIORITY 帧可用于更改流的优先级。
优先级的目的是允许端点在管理并发流时表达它希望其对等方如何分配资源。最重要的是,当发送容量有限时,可以使用优先级来选择传输帧的流。
流可以通过将它们标记为依赖于其他流的完成来确定优先级。每个依赖项都分配了一个相对权重,该数字用于确定分配给依赖于同一流的流的可用资源的相对比例。
显式设置流的优先级被输入到优先级处理过程中。它不保证流相对于任何其他流的任何特定处理或传输顺序。端点不能强制对等方使用优先级以特定顺序处理并发流。因此,表达优先级只是一个建议。
可以从消息中省略优先级信息。在提供任何显式值之前使用默认值
5. 服务器推送
HTTP/2 允许服务器预先向客户端发送(或“推送”)响应(连同相应的“承诺”请求)与先前客户端发起的请求相关联。当服务器知道客户端需要这些响应可用才能完全处理对原始请求的响应时,这会很有用。
客户端可以请求禁用服务器推送,尽管这是为每个跃点独立协商的。该SETTINGS_ENABLE_PUSH设置可以设置为0,表明服务器推送被禁用。
承诺的请求必须是可缓存的,必须是安全的,并且不得包含请求正文。收到不可缓存、未知安全或指示请求正文存在的承诺请求的客户端必须使用PROTOCOL_ERROR类型的流错误重置承诺流。请注意,如果客户端不认为新定义的方法是安全的,这可能会导致承诺的流被重置。
如果客户端实现了 HTTP 缓存,则可缓存的推送响应可以由客户端存储。推送的响应被认为在源服务器上成功验证(例如,如果存在“无缓存”缓存响应指令,而由承诺的流 ID 标识的流仍处于打开状态。
不可缓存的推送响应不得由任何 HTTP 缓存存储。它们可以单独提供给应用程序。
服务器必须在:authority伪标头字段中包含一个服务器对其具有权威性的值。客户端必须将服务器不具有权威性的PUSH_PROMISE视为PROTOCOL_ERROR类型的流错误。
中介可以接收来自服务器的推送并选择不将它们转发到客户端。换句话说,如何使用推送的信息取决于该中介。同样,中介可能会选择向客户端进行额外的推送,而无需服务器采取任何行动。
客户端无法推送。因此,服务器必须将PUSH_PROMISE帧的接收视为PROTOCOL_ERROR类型的连接错误。客户端必须通过将消息视为PROTOCOL_ERROR类型的连接错误来拒绝将SETTINGS_ENABLE_PUSH设置更改为0 以外的值的任何尝试。
5.1 推送请求
服务器推送在语义上等同于服务器响应请求;但是,在这种情况下,该请求也由服务器发送,作为PUSH_PROMISE帧。
所述PUSH_PROMISE帧包括包含了一套完整的请求报头字段的该服务器的属性对所述请求的首标块。无法推送对包含请求正文的请求的响应。
推送的响应总是与来自客户端的显式请求相关联。服务器发送的PUSH_PROMISE帧在该显式请求的流上发送。所述PUSH_PROMISE框架还包括一个承诺流标识符,从所述服务器中可用的流标识符选择。
PUSH_PROMISE和任何后续CONTINUATION帧中的头字段必须是一组有效且完整的请求头字段。服务器必须在:method伪标头字段中包含一个安全且可缓存的方法。如果客户端收到不包含完整有效的头字段集的PUSH_PROMISE或:method伪头字段标识了不安全的方法,则它必须以PROTOCOL_ERROR类型的流错误进行响应.
服务器应该在发送任何引用承诺响应的帧之前发送PUSH_PROMISE帧。这避免了客户端在接收任何PUSH_PROMISE帧之前发出请求的竞争。
例如,如果服务器收到对包含多个图像文件的嵌入链接的文档的请求,并且服务器选择将这些附加图像推送到客户端,则在包含图像链接的DATA帧之前发送PUSH_PROMISE帧可确保客户端能够在发现嵌入链接之前查看资源将被推送。类似地,如果服务器推送标头块引用的响应(例如,在 Link 标头字段中),则在发送标头块之前发送PUSH_PROMISE可确保客户端不会请求这些资源。
PUSH_PROMISE帧不得由客户端发送。
PUSH_PROMISE帧可以由服务器发送以响应任何客户端启动的流,但该流必须处于相对于服务器的“打开”或“半关闭(远程)”状态。PUSH_PROMISE帧与组成响应的帧穿插在一起,但它们不能与组成单个标头块的HEADERS和CONTINUATION帧穿插在一起。
发送PUSH_PROMISE帧会创建一个新流,并将流置于服务器的“保留(本地)”状态和客户端的“保留(远程)”状态
5.2 推送响应
发送PUSH_PROMISE帧后,服务器可以开始将推送的响应作为响应传送到使用承诺流标识符的服务器启动的流上。服务器使用此流传输 HTTP 响应,使用与第 8.1 节中定义的相同的帧序列。在发送初始HEADERS帧后,此流对客户端变为“半关闭” 。
一旦客户端收到一个PUSH_PROMISE帧并选择接受推送的响应,客户端不应该在承诺的流关闭之前发出任何对承诺的响应的请求。
如果客户端出于任何原因确定它不希望接收来自服务器的推送响应,或者如果服务器开始发送承诺的响应时间过长,则客户端可以使用CANCEL或REFUSED_STREAM发送RST_STREAM帧代码并引用推送流的标识符。
客户端可以使用SETTINGS_MAX_CONCURRENT_STREAMS设置来限制服务器可以同时推送的响应数量。将SETTINGS_MAX_CONCURRENT_STREAMS值设为零会通过阻止服务器创建必要的流来禁用服务器推送。这不会禁止服务器发送PUSH_PROMISE帧;客户端需要重置任何不需要的承诺流。
接收推送响应的客户端必须验证服务器是否具有权威性或为相应请求配置了提供推送响应的代理。例如,只为example.com DNS-ID 或 Common Name提供证书的服务器不允许推送对https://www.example.org/doc的响应。
PUSH_PROMISE流的响应以HEADERS帧开始,它立即将流置于服务器的“半关闭(远程)”状态和客户端的“半关闭(本地)”状态,并以帧结束轴承 END_STREAM,它将流置于“关闭”状态。
注意:客户端永远不会为服务器推送发送带有 END_STREAM 标志的帧。
Reference
[1] HTTP/1的缺点总结
[2] RFC7540 (HTTP/2)
[3] HTTP/2