http 2.0协议

2018-09-21 fishedee 后端

1 概述

HTTP 2.0协议,这是一个设计上相当精妙的协议,在保留了上层的http使用者原来的语义外,大大提高了性能和功能。

2 目标

HTTP 2.0协议主要是针对现有的HTTP 1.1协议上的改进,他的主要目标为:

  • 降低延迟,
  • 提高并发,
  • 减少包大小
  • 避免文本格式带来的编码限制
  • 保留HTTP 1.1的语义,上层的应用程序不需要任何改动,也无需变更现有的网络基础设施

2.1 高延迟

HTTP 1.1版本中,每个资源一个连接,在同一个连接中,服务器只有在上一个请求处理完毕后,才能发送下一个请求的处理结果。

客户端:A资源的resquest->B资源的request->服务器
服务器:A资源的response->B资源的response->客户端

例如,在以上的例子中,在同一个连接中,客户端依次发送A资源和B资源的请求给服务器,服务器必须按照请求顺序首先发送A资源的响应,然后才能发送B资源的响应给客户端,响应的顺序必须严格一一对应请求的顺序。这意味着一旦A资源的处理较慢,就会拖慢B资源的返回结果。例如,如果A资源是一个需要向后端发送报表汇总结果的请求,那么即使B资源是一个获取静态文件资源的结果,都得被迫往后等,大大增加了B资源的请求响应时长,这让用户感受到了巨大的卡顿感觉。

连接一:

客户端:A资源的resquest->服务器
服务器:B资源的reponse->客户端

连接二:

客户端:B资源的resquest->服务器
服务器:B资源的reponse->客户端

要避免这种情况,在http 1.1中,你可以通过并发发送请求来减少延迟。也就是A资源的请求和B资源的请求是通过两个不同的tcp连接来获取的,那么B资源可以不需要等待A资源请求的返回就能直接发送响应结果了。

可是,每建立一个新的连接,都不可避免地需要tcp的三次握手,这代表一个B资源的请求响应延迟比复用同一个连接,至少多了一个往返延时(RTT)。这让目前的http 1.1协议陷入了一个死循环中:

  • 如果复用同一个连接,可以减少三次握手的往返延时,但是可能会陷入队首堵塞
  • 如果并发地使用多个连接,那不可避免地需要多了一个三次握手的往返延时

因此,我们希望在能避免三次握手的往返延的同时,也能避免的队首堵塞的新HTTP协议。

2.2 低并发

如果某个网站需要并发地获取大量的资源,在目前的http 1.1协议中,浏览器只能尽量多地并发开多个tcp连接与服务器相连。这对服务器来说简直是个噩梦,因为一个客户端开5个并发连接,那么只需2000个客户端,就能占有服务器的1万个socket端口,服务器很快就会因为open too many files错误而崩溃。

目前的解决办法是,在后端的web服务器前面搭建多个网关服务器,多个网关服务器前面使用LVS,HAProxy等工具做负载均衡,并且每个网关服务器用长连接的方式与后端的web服务器建立连接。当网关服务器收到请求后,透明转发给后端的web服务器来处理即可。这样后端的web服务器就能并发地处理多个请求,同时也能避免连接数过多的问题。

归根到底,http 1.1的高并发仅能依赖于多连接来实现的。因此,我们希望能并发地发送大量的请求的同时,能占用尽可能少的连接。

2.3 多冗余

Screen Shot 2018-10-08 at 11.09.44 P
Screen Shot 2018-10-08 at 11.09.34 P

http协议中包含两部分,头部和正文。请求报文的头部中,我们常见有Host,User-Agent,Accept,Accept-Encoding等等的这些字段,在响应报文中,我们常见有Connection,Server,Vary等等的这些字段。这些头部信息在不同资源的报文中,数据大多是重复而且是无压缩的,即使是发送一个Hello World的返回报文,但头部都已经占有很大一部分的数据量。

因此,我们希望头部信息是能压缩和恰当省略的,以避免占用过多的网络资源。

2.4 受限的文本格式

http 1.1的请求和响应报文都是文本格式的,头部和正文之间用,正文的结束也是用。采用文本格式的好处是简单易读,容易抓包调试,但对程序开发来说,就很头痛了,我们需要特别小心,报文的正文部分不能包含,否则会让报文陷入提前终止的景况。例如,上传post表单时,我们用x-www-form-urlencoded格式时,就需要将value字段用encodeURIComponent来转义,用multipart/form-data格式时,我们需要引入boundary来决定新的报文边界。转义和boundary的引入给程序开发带来不必要的麻烦,而且也让网络占有了无用的带宽。

因此,我们希望能正文可以直接支持上传二进制格式,协议的主体是以二进制来实现的,而不是文本格式来实现的。

3 特性

3.1 二进制分帧

http 2.0的第一个改进是将一个请求响应的处理划分为更小的部分,称为消息,流和帧。

  • 流,一次请求,和对应的响应称为一个流。例如,发送A资源的请求报文,和回复A请求的响应报文称为一次流。下一次发送发送B资源的请求报文,和回复B请求的响应报文称为另外一次流,这两次流的id是不一样的。
  • 消息,一次流由两个消息组成,分别是请求报文消息和响应报文消息。http 2.0中要求无论是客户端发送的请求,还是服务器主动推送的请求,都必须符合一个请求消息对应一个响应消息的结构
  • 帧,每个消息由多个帧组成,常见的帧是头部信息帧和正文信息帧,头部信息帧和正文信息帧豆可以由多个帧组合而成的

这是常见的一个请求报文,在http 2.0中被划分为两个帧来发送,头部帧,和正文帧,要注意的是,请求行和头部字段都被合并都一个头部帧里面了。

http 1.1中的请求行信息 http 2.0中的头部字段
方法 :method
位置 :path
协议 :scheme
状态码 :status

每个帧的定义如上,24位的长度,8位的帧类型,8位的帧选项,32位的流ID。并且,由于每个帧都有一个长度字段,这代表着可以不再需要content-length字段了,也不再强制要求以,我们甚至可以简单地直接传送一个二进制的数据。

因此,http2解决了http1.1中的受限的文本格式的问题

3.2 多路复用

在有了二进制分帧的改进后,http2引入多路复用的特性,使用流ID来区别不同的请求消息对应的响应消息,不同流的请求消息和响应消息甚至可以乱序发送,一举解决了http 1.1中的高延迟和低并发的问题。

首先,http 2.0中要求客户端发送的请求,流ID必须要是奇数,而服务器发送的请求,流ID必须要是偶数。至于不同请求对应的响应消息,其流ID必须是与对应的请求消息的流ID是一致的。例如,在上面的例子,客户端已经发送了流1和流2的请求消息,服务器需要向客户端发送流1和流2的响应消息。服务器先发了流1的数据帧,然后交错地先发送流2的头部帧和数据帧,并在流2的数据帧中FLAGS字段加上END_STREAM标志,以表示流2的所有响应信息都已经发送了。而后,服务器才发送流1的数据帧。为什么,服务器可以这样做,不需要像http 1.1一样,在完全发送了流1的响应消息后才发送流2的响应消息,那是因为http 1.1中每个请求响应循环中缺少一个流ID字段,客户端无法区分这次收到的响应信息是哪一次发出的请求的响应结果。另外,http 2.0提供了二进制分帧特性,这代表了服务器可以发送到流1的结果的中途,就可以切换到发送到流2的响应结果去,让紧急的或简单的请求报文可以得到优先的回复,避免了http 1.1中的头部堵塞的问题。当然,要注意的一个问题是,帧是http2中的最小分割单位了,你不能在发送一个帧的中途插入发送另外一个帧,这是不允许的。

因此,在http2中,客户端可以通过简单地将多个需要的并发请求直接复用同一个tcp连接就可以了,既不会导致头部堵塞的问题,也不会导致建立多个连接所需的三次握手的延迟问题。因此,http2解决了http1.1中的高延迟和低并发的问题。

3.3 首部压缩

http2中对头部信息进行了两个优化

  • 头部信息默认使用HPACK压缩算法进行压缩,避免了文本格式导致的传输低效率的问题
  • 在一个连接中,客户端和服务器共同渐进地更新维护一个首部表,发送端在每一次的传输的头部信息时,只传输与上一次的头部信息不一样的部分。要么是在上次头部信息中替换一个值,要么是增加一个值。(删除值相当于替换为空值)

3.4 升级与兼容

http2协议上支持在http和https上的http 2.0协议

3.4.1 兼容连接

客户端在不知道服务器是否支持http 2.0协议时,可以通过两个方式来确定

  • 在https环境下,通过在tls握手的ALPN扩展来询问,如果服务器支持http2,则在返回的ALPN扩展中返回确认h2协议的ALPN扩展。
  • 在http环境下,通过用一个GET请求附带Upgrade字段来询问,如果服务器支持http2,则返回101的响应报文,如果服务器不支持http2,则直接返回普通200的响应报文就可以了

在以上两种方式中,如果客户端收到服务器支持http2协议的信息后,则会进入下一节的协议建立阶段。

3.4.2 直接连接

http2协议建立阶段

  • 客户端发送固定的PRI方法的普通报文,以代表连接序言
  • 客户端发送SETTING帧,而后可以马上发送HTTP/2其它帧
  • 服务器接收到客户端的连接序言之后,需要发送SETTING帧,并可以马上发送HTTP/2其它帧
  • 任一端接收到SETTINGS帧之后,都需要返回一个包含确认标志位SETTIGN作为确认
  • 其它帧的正常传输

就这样,http 2协议支持了原有的http 1.1基础设施保持不变的情况下,实现平滑升级

3.5 其他

http 2协议的SETTING帧可以设置接收窗口大小,设置并发发送帧数量等选项,实现流量堵塞控制。另外,HEADER帧和DATA帧可以设置优先级大小,实现优先级处理控制。

4 帧格式

4.1 Setting帧

+-------------------------------+
|       Identifier (16)         |
+-------------------------------+-------------------------------+
|                        Value (32)                             |
+---------------------------------------------------------------+

SETTINGS帧的有效载荷包含0个或多个参数,每个参数由一个无符号16位标识和一个无符号32位值构成

4.2 Header格式

+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|E|                 Stream Dependency? (31)                     |
+-+-------------+-----------------------------------------------+
|  Weight? (8)  |
+-+-------------+-----------------------------------------------+
|                   Header Block Fragment (*)                 ...
+---------------------------------------------------------------+
|                           Padding (*)                       ...
+---------------------------------------------------------------+

各个字段的解释如下:

Pad Length(填充长度):8比特位字段,表示填充字节序列(即上图中的Padding)的字节长度。这个字段是可选的,只有在设置了PADDED标志位时才有。 E(排他标志位):1比特位字段,表明流依赖是排他的。这个字段是可选的,只有在设置了PRIORITY标志位时才有。 Stream Dependency(流依赖):31比特位的流标志位,表示当前流依赖的那个流,即当前流的“父亲流”。这个字段是可选的,只有在设置了PRIORITY标志位时才有。 Weight(权重):8比特位的整数,表示流的优先级权重,范围是1-256之间。这个字段是可选的,只有在设置了PRIORITY标志位时才有。 Header Block Fragment(报头块碎片):报头块的片段。 Padding(填充字节):填充字节。

HEADERS帧定义了以下标志位:

END_STREAM(0x1):设置此标志位时表明当前帧是端点在流上发送的最后一个帧。携带END_STREAM标志位的HEADERS帧预示流的结束。然而,在相同的流中,携带END_STREAM标志位的HEADERS帧后面可以跟随一个或多个CONTINUATION帧。逻辑上,CONTINUATION帧是HEADERS帧的一部分。 PADDED (0x8):设置此标志位时表明帧中包含“填充长度”和“填充字节”字段。 PRIORITY(0x20):设置此字段时表明帧中包含“排他标志位”、“流依赖”和“权重”字段。

HEADERS帧的有效载荷包含一个报头块碎片。如果HEADERS帧中装不下报头块碎片,那么报头块碎片的剩余部分将被装到后面的CONTINUATION帧中。

HEADERS帧必须与一个流关联起来。如果接收到的HEADERS帧的流标志位是0x0,那么接收方必须响应一个类型是PROTOCOL_ERROR的连接错误。

HEADERS帧会改变流的状态。

HEADERS帧可以包含填充字节。填充字段和标志位与DATA帧的相同。填充字节的长度超过为报头块碎片保留的大小时,将导致一个类型是PROTOCOL_ERROR的连接错误。

HEADERS帧中的优先级信息逻辑上等价于一个单独的PRIORITY帧,但是,这些信息包含在HEADERS帧中可以避免在创建新流时潜在的优先级信息丢失。HEADERS帧中的“优先级”字段紧接着流中的第一个,重新安排流的优先级。

4.3 Data格式

+---------------+
|Pad Length? (8)|
+---------------+-----------------------------------------------+
|                            Data (*)                         ...
+---------------------------------------------------------------+
|                           Padding (*)                       ...
+---------------------------------------------------------------+

各个字段解释如下:

Pad Length(填充长度):8比特字段,表示填充字节序列(即上图中的Padding)的字节长度。这个字段是可选的,只有在设置了PADDED标志位时才有。 Data(数据):具体应用数据,即帧的有效载荷减去其它字段的长度(其它字段是可选的)。 Padding(填充字节):填充字节不包含任何应用语义值。发送时必须将填充字节的值都设置为0。接收方没有义务验证填充序列,但是可以将非0的填充字节视为类型为PROTOCOL_ERROR的连接错误。

DATA帧定义了以下标志位:

END_STREAM (0x1):设置此标志位时表明当前帧是端点在流上发送的最后一个帧,从而导致流进入“半关闭”或“关闭”状态。 PADDED (0x8):设置此标志位表明帧中包含“填充长度”和“填充字节”这2个字段。

DATA帧必须与一个流关联起来。如果接收到的DATA帧的流标志位是0x0,那么接收方必须响应一个类型是PROTOCOL_ERROR的连接错误。

DATA帧受流量控制支配。只有当流处于“打开”或“半关闭(远程)”状态时,才能发送DATA帧。整个DATA帧都包含在流量控制中,数据、填充长度和填充字节字段都不例外。如果接收到的DATA帧关联的流不是“打开”或“半关闭(本地)”状态,那么,接收方必须响应一个类型是STREAM_CLOSED的流错误。

“填充字节”的总数由“填充长度”字段决定。如果填充字节序列的长度等于或大于整个帧的有效载荷,那么,接收方必须将其视为一个类型是PROTOCOL_ERROR的连接错误。

一个特殊情况是,可以通过将“填充长度”字节的值设置为0,为DATA帧增加一个字节的长度。

5 总结

http 2.0的最佳参考资料是golang中关于http 2.0的实现,相当的简单易懂和优雅。因此,参照着,我也写了一个简单的http 2.0协议客户端的Demo

参考资料:

相关文章