Server Push
,即服务器推送,rfc7540#section-8.2 中有详细的描述。简单来说,HTTP/2.0
允许 Server 抢先发送/推送Response
,以及相应的promise
请求给 Client,并且与 Client 之前发起的请求相关联。
使用场景也有限制,前提是 Server 知道 Client 需要哪些资源,推送规则一般是提前配置好的。
No Server Push
在HTTP/1.X
里,没有Server Push
,假如一个index.html
页面包含三个资源:html代码
、css文件
、js文件
。那么如果要完全加载这个页面,就需要三次Request-Response
,也就是3个RTT,流程如下:
Server Push 原理
我们先看看HTTP/2.0
里的Server Push
表现,还是刚才的例子,一个index.html
页面包含三个资源:html代码
、css文件
、js文件
,如果请求index.html
(那么在命中Server Push
的情况下),流程如下图:
对照上图,我们发现,同样是加载index.html
,Server Push
流程里少了两个(style.css
、main.js
)请求,也就减少了页面加载时间。
总结一下,Server Push
的推送原理如下:
-
Client 发起请求
request
。 -
当 Client 的请求
request
命中Server Push
规则后,Server 首先会响应一个PUSH_PROMISE
帧,也就是承诺会在一个新的Stream
流上推送资源。 -
Server 在当前请求的
Stream
流上响应当前请求。 -
Server 在承诺的
Stream
流上推送具体的资源。
接下来我们看下Server Push
里关键的两个frame
帧。
PUSH_PROMISE
推送承诺帧(rfc7540#section-6.6)是Server Push
的关键,目的是告诉 Client,即将在某个Stream
上推送资源。Payload
格式如下:
+---------------+
|Pad Length? (8)|
+-+-------------+-----------------------------------------------+
|R| Promised Stream ID (31) |
+-+-----------------------------+-------------------------------+
| Header Block Fragment (*) ...
+---------------------------------------------------------------+
| Padding (*) ...
+---------------------------------------------------------------+
1. Pad Length
8位字段,包含帧的长度,以八位字节为单位的填充。仅当PADDED
标志被设置。
2. R
单个保留位。
3. Promised Stream ID
无符号的31
位整数,用于标识承诺的流Id
。
4. Header Block Fragment
标头块片段,包含请求标头字段,承诺推送的资源路径。
5. Padding
填充八位字节。
RST_STREAM
流重置帧(rfc7540#section-6.4)。如果 Client 收到了推送承诺PUSH_PROMISE
帧,而且本地检测到了对应资源缓存,那么 Client 会发送RST_STREAM
(Error Code
= REFUSED_STREAM
),拒绝该流,也就是告诉 Server,我不需要该资源了,别给我推送了。
RST_STREAM
帧的Payload
格式如下:
+---------------------------------------------------------------+
| Error Code (32) |
+---------------------------------------------------------------+
1. REFUSED_STREAM:表示拒绝流。
2. CANCEL:表示取消流。
当然还有其他Code
,具体可以看官方文档 rfc7540#section-6.4。
推送竞争问题
Server Push
一定能减少页面加载(请求响应)时间吗?答案是否定的。因为 Server 在响应页面的请求后,就立即开始在承诺的Stream
流上推送了,尽管 Client 检测到本地有缓存,发送了RST_STREAM
帧拒绝了该流的推送。但是由于时序问题,Server 在收到RST_STREAM
之前,可能已经开始推送了,这样就造成了服务器带宽的浪费。就像下面这张图:
我们来做个测试,重复加载一个带Sever Push
的页面,因为Chrome
对资源有缓存机制,所以Client 会发送RST_STREAM
帧。Wireshark
实际数据包如下图:
如上图,Client 发送了RST_STREAM[6]
、RST_STREAM[8]
,但是 Server 已经开始在Stream6
、Stream8
上推送了部分数据,所以这种情况反而造成了带宽浪费。
推送测试
最后我们从0开始,来做一个测试,看看Server Push
和No Server Push
的各自表现,这里我们以静态资源为例。
1. 准备资源
首先在服务器上准备两个html
页面:serverpush.html
、noserverpush.html
,内容一致如下:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="/js/main.js"></script>
<link rel="stylesheet" href="/css/style.css">
<title>Server Push</title>
</head>
<body>
<p>hello server push</p>
</body>
</html>
接着准备/js/main.js
、/css/style.css
文件。
2. 规则配置
接着配置Server Push
规则,这里以nginx
容器为例,修改nginx.conf
,增加push
规则:
location /serverpush.html {
root /root/tomcat/html;
index index.html index.htm;
http2_push /css/style.css;
http2_push /js/main.js;
}
保存重启nginx
(nginx -s reload
)。
3. 开始测试
接着我们请求 www.laoqingcai-已注销.com/serverpush.… ,通过Wireshark
看到数据包交换流程如下:
如上图,整个推送流程也很简单:
-
Client 请求
/serverpush.html
,打开Stream 1
。 -
Server 在
Stream 1
上响应PUSH_PROMISE
,承诺会在Stream 2
上推送/css/style.css
,在Stream 4
上推送/js/main.js
。 -
Server 在
Stream 1
上响应serverpush.html
。 -
Server 在
Stream 2
上推送承诺的资源/css/style.css
。 -
Server 在
Stream 4
上推送承诺的资源/js/main.js
。
整个过程也和我们分析的推送原理一致。针对 noserverpush 的抓包图这里就不贴了,有兴趣的可以自己用Wireshark
抓包看下。
4. 结果对比
接着我们来看一下Server Push
实际带来的好处。截了两张图,第一张是带Server Push
的,第二张是No Server Push
的。
经过多次测试对比(排除网络抖动的影响),我们发现,带Server Push
的网页加载明显要比不带Server Push
的要快20-30ms
左右。
通过Chrome devtools
可以看到,Server Push
的资源加载过程少了几个阶段:
1. Stalled
连接的停滞阶段,即连接等待阶段,包含连接协商过程等等。
2. Request Sent
请求发送时间。
3. Waitting(TTFB)
请求发送后到收到 Server 响应的第1个Byte
的耗时,受网络状况和 Server 处理能力影响。
其实归根结底,还是因为 Server Push 减少了页面内部的(资源)请求数量,从而减少了请求等待时间,以及请求往返次数(RTT),毕竟带宽不是瓶颈。
总结
最后我们总结一下,使用Server Push
可以减少页面资源加载的RTT
次数和加载的等待时间,最终减少页面的加载时间。特别是静态资源比较多的情况。
但是目前 Client 针对静态资源都有缓存机制,而且缓存的时间也比较长,如果使用Server Push
,可能会出现推送竞争的情况,提前推送反而会浪费服务器带宽,所以在使用这个特性的时候需要仔细的考虑实际效果和带来的问题。