HTTP/2,简称h2,是HTTP协议在1999年发布了HTTP1.1之后的首个更新。它主要基于谷歌的SPDY协议,于2015年5月正式发表,大多数主流浏览器已经在2015年底前支持了该协议。HTTP/2的最显著的改进主要体现在多路复用、允许设置请求优先级、压缩算法、二进制传输等等,这些特性能够让使用HTTP/2的应用获得更快、更简单、更稳定的效果。目前流行的RPC框架gRPC也采用了HTTP/2作为传输方式。
HTTP/2协议没有改动HTTP原来的应用层语义。之前熟悉的请求方法、状态码、URI、http headers等概念还是一模一样的。但HTTP/2修改了数据包的格式以及数据在客户端与服务端之间传递的方式。所以只要服务端和客户端(浏览器)支持,应用不需要经过任何修改就可以切换到新协议下运行。
有人可能会问,为什么这个要叫做HTTP/2而不是HTTP/1.2呢?原因主要是HTTP/2引入了一个二进制分帧层(Binary Framing Layer,这层是无法和之前的HTTP/1.x的服务器和客户端兼容的,所以HTTP的协议的主版本就升级到了2。那接下来我们就来了解一下这个二进制分帧层。
二进制分帧层(Binary Framing Layer)
新的二进制分帧层是HTTP/2性能增强的核心,他定义了消息的封装格式与传输方式
为了说明这个传输的过程,我们需要先理解3个概念
数据流(Stream):数据流定义了在一条TCP连接内的逻辑上的双向数据流,在一个数据流上可以承载一条或多条消息。一个TCP连接上可以承载很多数量的双向数据流。
消息:与HTTP请求或响应消息对应的一系列帧,这个和传统的request/response是对应的。
帧(frame):HTTP/2通信的最小单位,每个帧都包含帧头,帧上会标明自己所属的数据流。来自不同数据流的帧可以交错发送。在HTTP/2中定义了很多种类的帧,最主要的有HEADER帧与DATA帧,对应了原来的http header和body。
简单的说,HTTP/2协议将HTTP/1.x中使用文本方式传递的请求与响应消息分解成为了二进制编码的帧的传递。所有这些帧能够复用一个TCP连接,通过任意数量的逻辑数据流进行传输。这也是HTTP/2协议性能与功能改进的基础。
连接多路复用与共享
在HTTP/1.x中,如果要并发请求多个资源,那必须使用多个TCP连接,现代浏览器对单个网站能同时发起的连接数都做了限制。HTTP1.1虽然允许在同一个TCP连接上一次发送多个请求,但是他规定了服务器的响应的发送要根据请求被接收的顺序进行排队,也就是说,先接收到的请求的响应也要先发送。这样如果先收到的请求处理时间长的话,就会阻塞后面的请求的响应的发送。而HTTP/2彻底解决了这个问题,所有请求和响应都可以分成互不依赖的帧在同一TCP连接上通过不同的逻辑数据流自由发送,由接收端收到帧之后再组装起来。
在这个图中我们可以看到,有三个数据流在同时传输。
通过多路复用,我们就可以做到一个origin一个连接。实际应用场景中,大多数HTTP请求都是短暂的,而TCP协议却是为了长时间的数据传输设计的,所以复用一个TCP连接可以显著的提高性能,降低成本。对TLS连接来说,也可以减少加密连接建立的开销。
数据流的优先级
将HTTP消息分解为多个独立帧之后,这些帧就可以在客户端和服务器之间以任意顺序发送,那帧的传输顺序的先后和依赖关系就会很大地影响性能。HTTP/2通过定义数据流的优先级和依赖关系来解决了这个问题。HTTP/2支持给每个数据流赋值一个1~256之间的整数权重,并且可以定义一个数据流和另一个数据流之间的依赖关系。数据流的权重和依赖关系可以让数据流之间形成一个依赖关系树。客户端和服务端可以根据这个依赖关系树来决定发送的优先级。举例来说,在一个网页中,我们就可以设置HTML文件为高优先级,css与js为中优先级,图片为最低优先级。但这里需要注意的是,这个优先级只是一个偏好,而不是强制的,所以优先级并不能保证高优先级的一定比低优先级的先传输。这个也是非常合理的,因为我们不希望一个低优先级的资源因为一个高优先级的资源处理和传输比较慢而被完全阻塞住。
流控
在某些场景下,接收方负载比较重或者比较忙的时候,可能不希望服务端继续发送响应数据过来。比方说在比较长的视频的播放场景下,用户暂停了或者数据传输速度远大于播放速度,那我们可能不想提前缓冲太多的数据下来。另外,在代理服务器的场景下,如果下游的连接速度比较快,上游的速度比较慢,那代理也会希望能控制下游发送请求的速度,以免太多的请求在代理处堆积。TCP协议本身也支持流控,但是因为HTTP/2支持在一个TCP连接中复用多个数据流,所以仅仅依靠TCP协议的流控显然是不够的。因此HTTP/2引入了一个简单的流控机制,主要特点有:
流控是双向的,接收端和发送端都可以设置自己的流控窗口大小
流控是基于信任的,接收端在创建连接的时候会告知对方自己的流控窗口字节大小,每当发送端发送一个DATA帧的时候,这个窗口会相应减少,如果窗口减少到0,发送端需要主动停止发送数据,接收端可以通过发送新的WINDOW_UPDATE帧来修改当前流控窗口的字节数
流控无法被禁用。当连接建立的时候,双方会交换SETTINGS帧,定义了双方的流控窗口字节大小,默认的流控字节数是65,536字节,最大可以设置成2的32次方-1
流控是基于直接节点之间的,不是端到端的。所以中间层(代理)也可以来对上下游进行流控
基于上面的简单机制,HTTP/2就可以实现比较复杂的流控,比方说可以通过让浏览器设置窗口为0,使浏览器暂停某些资源的提取,等用户要求继续之后再开放窗口继续传输。
服务端推送
HTTP/2另一个强大的功能是可以允许服务端对一个请求发送多个响应,也就是说服务端可以在没有明确请求的情况下向客户端推送额外的资源。比方说请求一个网页的时候,涉及到很多资源,有静态页面、图片、css、js,传统的方式需要对每个资源分别请求,但如果服务端知道请求某个静态页面的时候都是要同时请求相对应的其他资源的,通过这种服务端推送的机制就能主动的把资源一起返回,从而节省客户端的很多请求时间。比方说一个网页请求style.css的时候,服务端就可以同时把style.js也推送给客户端,这样客户端在需要style.js的时候就可以直接从缓存读取,而不需要再通过网络请求了。
所有的服务端推送数据都是由PUSH_PROMISE帧发起的,它标明了服务端推送的资源的意图,并且该帧要在请求推送资源的响应帧之前传输。这个做法应该是比较好理解的,因为这样能避免客户端重复请求服务端主动推送的数据。服务端推送的使用方式由客户端掌握主动权,客户端可以限制并行推送的数量、调整流控窗口、或者禁用服务端推送。这些优先级都是在连接建立初期通过SETTINGS帧传输的,后续也可以随时更新。
头部压缩
在HTTP/1.x中,请求和响应头部始终是以纯文本方式传输,而不能被压缩的。这样每个请求和响应都会增加一些开销,特别是在启用了cookie的时候,可能每次会要增加几k字节的开销。HTTP/2采用了HPACK压缩格式来压缩请求和响应头。这种压缩格式不仅采用静态霍夫曼编码对头字段进行编码,还要求发送和接收方都维护一个动态header字段的索引的列表,通过这个索引列表,后续传输重复值的时候就可以只传输索引值,而不需要重复传输完整的信息。
TLS传输层加密
虽然理论上HTTP/2也是支持非加密连接传输的(这种非加密连接的HTTP/2简称为h2c),但实际上目前主流浏览器厂商都只实现了加密连接的模式,所以https变成了HTTP/2的事实上的标准。
一个例子
下面我们来看一个具体的例子,例子程序通过gRPC向服务端发起一次调用,并得到返回结果,从客户端的Netty日志中,我们可以看到HTTP/2连接建立与包传输的完整过程
[1] [id: 0x3c68f114, L:/127.0.0.1:57280 - R:localhost/127.0.0.1:50051] OUTBOUND SETTINGS: ack=false settings={ENABLE_PUSH=0, MAX_CONCURRENT_STREAMS=0, INITIAL_WINDOW_SIZE=1048576, MAX_HEADER_LIST_SIZE=8192} [2] [id: 0x3c68f114, L:/127.0.0.1:57280 - R:localhost/127.0.0.1:50051] OUTBOUND WINDOW_UPDATE: streamId=0 windowSizeIncrement=983041 [3] [id: 0x3c68f114, L:/127.0.0.1:57280 - R:localhost/127.0.0.1:50051] INBOUND SETTINGS: ack=false settings={MAX_CONCURRENT_STREAMS=2147483647, INITIAL_WINDOW_SIZE=1048576, MAX_HEADER_LIST_SIZE=8192} [4] [id: 0x3c68f114, L:/127.0.0.1:57280 - R:localhost/127.0.0.1:50051] OUTBOUND SETTINGS: ack=true [5] [id: 0x3c68f114, L:/127.0.0.1:57280 - R:localhost/127.0.0.1:50051] INBOUND WINDOW_UPDATE: streamId=0 windowSizeIncrement=983041 [6] [id: 0x3c68f114, L:/127.0.0.1:57280 - R:localhost/127.0.0.1:50051] INBOUND SETTINGS: ack=true [7] [id: 0x3c68f114, L:/127.0.0.1:57280 - R:localhost/127.0.0.1:50051] OUTBOUND HEADERS: streamId=3 headers=GrpcHttp2OutboundHeaders[:authority: localhost:50051, :path: /helloworld.Greeter/SayHello, :method: POST, :scheme: http, content-type: application/grpc, te: trailers, user-agent: grpc-java-netty/1.36.0, grpc-accept-encoding: gzip] streamDependency=0 weight=16 exclusive=false padding=0 endStream=false [8] [id: 0x3c68f114, L:/127.0.0.1:57280 - R:localhost/127.0.0.1:50051] OUTBOUND DATA: streamId=3 padding=0 endStream=true length=5 bytes=0000000000 2021-07-16 14:27:27,889 [9] DEBUG i.g.n.s.i.g.n.NettyClientHandler - [id: 0x3c68f114, [9] [id: 0x3c68f114, L:/127.0.0.1:57280 - R:localhost/127.0.0.1:50051] INBOUND HEADERS: streamId=3 headers=GrpcHttp2ResponseHeaders[:status: 200, content-type: application/grpc, grpc-encoding: identity, grpc-accept-encoding: gzip] padding=0 endStream=false [10] [id: 0x3c68f114, L:/127.0.0.1:57280 - R:localhost/127.0.0.1:50051] INBOUND DATA: streamId=3 padding=0 endStream=false length=13 bytes=00000000080a0648656c6c6f20
[1] 发送了一个SETTINGS帧,设置了禁用服务端推送、最大并发STEAM条数为0,初始流控窗口大小为1048576(1M)字节,最大Header大小为8192(8k)字节。
[2] 发送一个WINDOW_UPDATE帧,数据流Id为0,将流控窗口大小增加983041字节
[3] 收到服务端发送的一个SETTINGS帧,设置为最大并发数据流数量为2147483647,初始流控窗口大小为1048576(1M)字节,最大Header大小为8192(8k)字节。
[4] 发送SETTINGS帧,对服务端发送的SETTINGS进行ack
[5] 收到服务端WINDOW_UPDATE帧,数据流Id为0,将流控窗口大小增加983041字节
[6] 收到服务端SETTINGS帧,对客户端之前发送的SETTINGS进行ack
[7] 发送请求头HEADERS帧,可以看到header中的:method, content-type, user-agent等等都是和HTTP/1.x中是一样的
[8] 发送请求的DATA数据帧
[9] 收到响应的HEADERS帧,有传统的:status, content-type等响应头字段
[10] 收到响应的DATA数据帧
在nginx上开启HTTP/2
nginx提供了非常简便的开启HTTP/2的方式,只要nginx版本在1.9.5及以上,启用了http_v2_module和http_ssl_module模块,并且为网站配置了https。然后只需要修改
server { # 添加 http2 listen 443 ssl http2; ... }
重新加载配置后,网站就支持HTTP/2的方式访问了。
推荐本站淘宝优惠价购买喜欢的宝贝:
本文链接:https://hqyman.cn/post/5845.html 非本站原创文章欢迎转载,原创文章需保留本站地址!
休息一下~~