构建一个 Cache-aware 的 HTTP/2 Server Push 机制

660 阅读6分钟
原文链接: zhuanlan.zhihu.com
原文链接 Creating a Cache-aware HTTP/2 Server Push Mechanism | CSS-Tricks

BY JEREMY WAGNER ON NOVEMBER 28, 2016

如果前面你曾经阅读多HTTP/2相关的文章,那么你很有可能听说过Server Push. 没有的话,下面是Server Push的核心要点: Server Push 让服务器能够在客户端请求一个资源的时候,预先下发另外一个资源。要使用它的话,你需要一个支持HTTP/2的Web Server,然后你只需要将一个你希望Server Push的Link的响应头设置为:

Link: </css/styles.css>; rel=preload

如果这个规则设置为一个HTML资源--假定为index.html的响应头,服务器不仅仅发送index.html同时会把style.css也会在相响应里面发送回来。这样主动Push的行为能够消除服务器的返回的延迟,意味着文档可以更快地渲染,至少对于在前面css资源被主动Push的场景下,更快地渲染是成立的。你可以Push任何你小心脏想要的资源。

Server Push有一个问题是一些开发者可以从上面的场景中推断出来的:它可能不是Cache友好的。浏览器确实可以在接受到Push之后拒绝Push(RST_STREAM),同时一些服务器有自己的特有的缓和方法。例如,Apache的mod_http2module模块有H2PushDiarySize指令来尝试解决这个问题。 H2O Server 也有一个"Cache-aware Server Push" (the optimized HTTP/2 server) 的机制,这个机制下面H2O会把Server Push的资源的指纹存在Cookie里面。如果你使用H2O Web Server的话,这真的是一个天大的利好。不过对于很多应用的要求而言,H2O不一定是可选项。

如果你用了一个HTTP/2服务器但又没有解决Server Push的Cache问题,那么恭喜你,下面告诉你一个很少后端代码就可以解决问题的方案。

超级简单的Cache可知Server Push解决方案

我们假定你有一个Website用了HTTP/2,你希望Server Push几个资源,例如一个css文件和一个JS文件。让我们也假定这些资源的内容很少变化,同时这些资源在Cache-Control上有很长的max-age时间。如果上面正是你的场景,那么Bingo,这里有一个你能用的快速但是有点恶心的后端解决方案:

if (!isset($_COOKIE["h2pushes"])) {
    $pushString = "Link: </css/styles.css>; rel=preload,";
    $pushString .= "</js/scripts.js>; rel=preload";
    header($pushString);
    setcookie("h2pushes", "h2pushes", 0, 2592000, "", ".myradwebsite.com", true);
}

这个PHP的例子检查一个名为h2pushes的Cookie。如果这个访问者是一个新用户,那么Cookie检查会失败。失败后我们会创建对应的Link响应头,然后通过header函数应答。响应头设置后,我们会调用 setcookie创建一个cookie,这样后面用户继续访问的时候这个Cookie就可以防止我们发送多余的Push给用户。在上面的例子里面Cookie的失效是30天。30天后,这个处理过程会重新做一次。

服务器虽然不直接知道该资源是否在客户端缓存上,但是整个逻辑是通的。这些Cookie只会在用户访问该页面之后设置。在上述的Cookie设置的时候,资源将会被Push到客户端,同时资源的缓存策略将会起效。上述方法看起来非常美好,直到你要改一个资源....

更加灵活的可知缓存的Server Push解决方案

要是你运营了一个用Server Push但是资源频繁改变的业务呢?你希望资源不改变的时候不要出现那些多余的Push,但是你也希望资源真的改了之后,一定要Push过去,或者你有时候需要Push更多的资源。这样话,你需要多些几行代码:

function pushAssets() {
    $pushes = array(
        "/css/styles.css" => substr(md5_file("/var/www/css/styles.css"), 0, 8),
        "/js/scripts.js" => substr(md5_file("/var/www/js/scripts.js"), 0, 8)
    );

    if (!isset($_COOKIE["h2pushes"])) {
        $pushString = buildPushString($pushes);
        header($pushString);
        setcookie("h2pushes", json_encode($pushes), 0, 2592000, "", ".myradwebsite.com", true);
    } else {
        $serializedPushes = json_encode($pushes);

        if ($serializedPushes !== $_COOKIE["h2pushes"]) {
            $oldPushes = json_decode($_COOKIE["h2pushes"], true);
            $diff = array_diff_assoc($pushes, $oldPushes);
            $pushString = buildPushString($diff);
            header($pushString);
            setcookie("h2pushes", json_encode($pushes), 0, 2592000, "", ".myradwebsite.com", true);
        }
    }
}

function buildPushString($pushes) {
    $pushString = "Link: ";

    foreach($pushes as $asset => $version) {
        $pushString .= "<" . $asset . ">; rel=preload";

        if ($asset !== end($pushes)) {
            $pushString .= ",";
        }
    }

    return $pushString;
}

// Push those assets!
pushAssets();

好吧,这可能比一点点代码多了一些,不过还是很直观的。我们先定义了一个可以驱动cache-aware server push行为的pushAssets函数。这个函数一开始我们定义了一个数组放我们所有想要Push的资源。由于我们希望重新Push那些改变过的资源,所以我们需要生成各个资源的指纹(文件MD5的前8个字节)以用于后面比较。例如,如果你提供了style.css,然后你修改了这个文件,你会通过查询字符串(例如:/css/styles.css?v=1)来确保浏览器不会用旧的版本。在这种这个例子里面,我们用了 md5_file函数来计算一个资源内容的校验和。由于md5校验和是32字节,我们只取了8个字节。任何时候资源修改后,校验和都会改变,这样资源会自动区分版本。


接着下来是重要的工作:跟前面一样,我们会检查h2pushes cookies是否存在。如果它不存在,我们会使用buildPushString工具函数来生成那些在$pushed数组的资源的link的响应头,然后用header函数来设置响应头。然后我们创建cookie,不过这一次我们会通过json_encode来对$pushed数组生成一个json字符串,然后存在cookie里面。我们其实也可以 serialize这个值,但是这个链接 说反序列化可能存在严重的安全风险,所以我们宁愿自己写一个更加安全的函数。

终于到了有趣的部分:那些重新访问的用户。如果用户重新访问之后他们的请求头会有h2pushes cookie,我们json_encode那个$pushes数组,然后与用户的cookie比较,如果两个值没有差异,那么Bingo,我们不需要Push任何资源。如果有差异,我们会找到差异的资源。上面我们用了json_decode函数来从cookie转换回数组,然后用 array_diff_assoc找到两个数组的差异。

用array_diff_assoc返回的差异,我们用buildPushString函数重新生成需要重新Push的资源。这样响应头发回给客户端,cookie也会通过新的$pushes数组生成。恭喜你,你刚刚学会了如何建立你自己的缓存可知的Server Push机制。

结论

通过一些小技巧,我们就可以为那些页面的老用户尽可能避免无效的Push。如果你用不上H2O等高档货,这个方案可能可以解决你的问题。这个方案目前在我自己的网站使用,它看起来不错。维护起来其实也很方便。修改资源之后,资源的指纹是自动更新,唯一需要改变的是增加push资源。

有一件事需要记住的是,随着浏览器在Server Push逐渐成熟,他们很可能更好识别出那些他应该直接从缓存中读取资源而不是接受Push的情况。如果浏览器没有做好这样的事情,HTTP/2服务器也很可能会想H2O那样实现Cache-aware Pushing机制。不过在那天到来之前,你只能靠自己了。上面的代码虽然是用PHP写的,不过移植为其他语言应该还是蛮简单的。

Happy pushing!

译者注:这里面还使用了Preload feature