创建缓存感知的 HTTP/2 服务器推送机制

Avatar of Jeremy Wagner
Jeremy Wagner

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 200 美元的免费积分!

如果您一直在阅读有关 HTTP/2 的内容,那么您可能听说过服务器推送。 如果没有,以下是它的要点:服务器推送允许您在客户端请求另一个资源时抢先发送一个资源。 要使用它,您需要一个支持 HTTP/2 的 Web 服务器,然后您只需为要推送的资源设置一个 Link 标头,如下所示

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

如果此规则设置为 HTML 资源的响应标头,例如 index.html,则服务器不仅会传输 index.html,还会在回复中传输 styles.css。 这消除了来自服务器的往返延迟,这意味着文档可以更快地渲染。 至少在这种情况下,CSS 被推送。 您想推送什么都可以。

服务器推送的一个问题是,一些开发人员推测,它可能并非在所有情况下都对缓存感知,具体取决于许多因素。浏览器确实有能力拒绝推送,并且一些服务器有自己的缓解机制。 例如,Apache 的 mod_http2 模块有 H2PushDiarySize 指令,它试图解决这个问题。 H2O 服务器 有一个名为“缓存感知服务器推送”的东西,它将推送资源的指纹存储在 cookie 中。 这是一个好消息,但前提是您实际上可以使用 H2O 服务器,具体取决于您的应用程序需求,这可能不是一种选择。

如果您使用的是尚未解决此问题的 HTTP/2 服务器,请不要担心。 您只需使用少量后端代码就可以轻松解决此问题。

一个非常基本的缓存感知服务器推送解决方案

假设您有一个在 HTTP/2 上运行的网站,并且您正在推送一些资源,例如 CSS 文件和 JavaScript 文件。 假设这些内容很少更改,并且这些资源在 Cache-Control 标头中具有很长的 max-age 时间。 如果您的情况符合此描述,那么您可以使用这种快速且肮脏的后端解决方案

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 的过期时间为 30 天(2,592,000 秒)。 当 cookie 过期(或被删除)时,该过程会重新发生。

这在严格意义上并非“缓存感知”,因为服务器无法确定资源是否缓存在客户端,但逻辑是相似的。 仅当用户访问过该页面时才会设置 cookie。 当它被设置时,资源已经被推送,并且由 Cache-Control 标头设置的缓存策略正在生效。 这非常有效。 当然,直到您必须更改资源为止。

一个更灵活的缓存感知服务器推送解决方案

如果您运行的是使用服务器推送的网站,但资源经常更改怎么办? 您希望确保不会出现冗余推送,但也希望在资源发生更改时推送资源,或者您可能希望稍后推送其他资源。 这需要比我们之前的解决方案更多的代码

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();

好吧,也许它不仅仅是一点代码,但它仍然是可以理解的。 我们首先定义一个名为 pushAssets 的函数,该函数将驱动缓存感知的推送行为。 此函数首先定义一个我们要推送的资源数组。 因为我们希望在资源发生更改时重新推送资源,所以我们需要对它们进行指纹识别,以便稍后进行比较。 例如,如果您正在提供名为 styles.css 的文件,但您对其进行了更改,则您将使用查询字符串(例如 /css/styles.css?v=1)对资源进行版本化,以确保浏览器不会提供过时的版本。 在这种情况下,我们使用 md5_file 函数根据其内容创建资源的校验和。 由于 md5 校验和为 32 字节,因此我们使用 substr 将其缩短为 8 个字节。 每当这些资源发生更改时,校验和也会发生更改,这意味着资源会自动进行版本化。

现在是重头戏:像以前一样,我们将检查 h2pushes cookie 是否存在。 如果不存在,我们将使用 buildPushString 辅助函数从我们在 $pushes 数组中指定的资源构建 Link 标头字符串,并使用 header 函数设置标头。 然后,我们将创建 cookie,但这次我们将使用 json_encode 函数创建 $pushes 数组的可存储表示形式,并将该值存储在 cookie 中。 我们可以使用 serialize 序列化此值,但这会在我们稍后使用 unserialize 反序列化它时带来 潜在的严重安全风险,因此我们应该坚持使用更安全的方法,例如 json_encode

现在是有趣的部分:如何处理返回的访问者。 如果事实证明访问者正在返回,并且有一个 h2pushes cookie,我们将使用 json_encode$pushes 数组进行编码,并将此 JSON 编码的数组的值与存储在 h2pushes cookie 中的值进行比较。 如果没有差异,我们什么也不做,继续愉快地进行。 但是,如果有差异,我们需要找出发生了什么变化。 为此,我们将使用 json_decode 函数将 h2pushes cookie 值转换回数组,并使用 array_diff_assoc 查找 $pushes 数组和 JSON 解码的 $oldPushes 数组之间的差异。

使用 array_diff_assoc 返回的差异,我们使用 buildPushString 辅助函数再次构建要再次推送的资源字符串。 标头被发送,并且 cookie 值使用 $pushes 数组的 JSON 编码内容更新。 恭喜。 您刚刚学习了如何创建自己的缓存感知服务器推送机制!

结论

通过一点巧思,推送资源的方式并不难,这种方式可以最大限度地减少重复访问者的冗余推送。 如果您没有使用像 H2O 这样的 Web 服务器的条件,那么此解决方案可能足够满足您的需求。 它目前正在我的网站上使用,并且似乎运行良好。 维护成本也很低。 我可以在我的网站上更改资源,使用指纹识别机制,资源引用会自行更新,推送会适应资源的变化,而无需我做任何额外的工作。

需要记住的一件事是,随着浏览器的成熟,它们可能会更好地识别何时应该拒绝推送,并从缓存中提供服务。 如果浏览器未能完善这种行为,HTTP/2 服务器可能会像 H2O 一样为用户实现一些缓存感知推送机制。 但是,在此之前,您可能需要考虑一下。 虽然它是用 PHP 编写的,但将此代码移植到其他后端语言应该是微不足道的。

推送愉快!


Cover of Web Performance in Action

Jeremy WagnerWeb Performance in Action 的作者,这是 Manning Publications 即将出版的一本书。 使用优惠券代码 csstripc 可以享受 38% 的折扣,或者任何其他 Manning 书籍。

在 Twitter 上关注他:@malchata