如果您一直在阅读有关 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 编写的,但将此代码移植到其他后端语言应该是微不足道的。
推送愉快!
很酷
非常好。 我是加密新手,所以这可能不是问题,但 MD5 碰撞有任何风险吗? http://stackoverflow.com/questions/1999824/whats-the-shortest-pair-of-strings-that-causes-an-md5-collision
我能想到的最坏情况是,您的代码片段会认为客户端拥有更新的资源,即误报。 再说一次,我不太了解这些东西,所以它可能不是问题。 我很喜欢 SO 线程中的这条评论。“开发人员假设哈希的唯一性是一个很好的 WTF 错误来源。”
这是一个非常好的问题。我确信在 8 字节的空间内,可能会发生碰撞,但我认为这个空间足够大,这将是一个极端情况。此外,真正重要的是“自上次推送以来,X 资产是否已发生更改?”。一旦资产被推送并且 Cookie 被设置,在资产再次更新之前,一切应该保持稳定。
如果您觉得理解有困难,或者您认为我可能遗漏了什么,请告诉我。
您永远不会*遇到冲突。这几乎是不可能的。MD5 不应用于加密函数。
除了 MD5,您还可以使用文件的修改时间来检测更改(据我所知,这在所有设置中并不总是可靠的更新)。
关于帖子中的第二部分,对于高流量网站,尤其是对许多文件进行校验和时,我会谨慎使用它。这种增加的 I/O 负载可能会对服务器负载/响应时间产生负面影响。
*在您的一生中
抱歉,最后一行之前的星号不知何故消失了。