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,可能会出现推送竞争的情况,提前推送反而会浪费服务器带宽,所以在使用这个特性的时候需要仔细的考虑实际效果和带来的问题。