[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"content-nginx-koa-use-http2-server-push":3,"$f27kfpbwjs7b85":22},{"id":4,"type":5,"slug":6,"title":7,"date":8,"category":9,"tags":10,"body_markdown":14,"permalink":15,"excerpt_src":15,"media_type":15,"media_title":15,"media_author":15,"media_url":15,"rating":15,"layout":15,"pv":16,"admin_only":16,"created_at":17,"updated_at":17,"deleted_at":15,"status":18,"html":19,"excerpt":20,"cover":21},183,"article","nginx-koa-use-http2-server-push","Nginx + Koa 开启http\u002F2 server push","2018-05-15 09:25","web",[11,12,13],"node.js","nginx","http2","\n一看这标题就是不准备好好写的。对的，最近特别忙，只能简单记录一下折腾的东西。\n\n## Nginx开启server push\n\n1. 升级nginx到1.13.9或以上版本（注意1.13.6修改http\u002F2的实现，与一些旧版本客户端不兼容，比如旧版Android okhttp）\n2. nginx配置中加上`http2_push_preload on`，表示使用preload header来作为server push标识\n\n## Node开启server push\n\nNode处理文件内容时加上preload header即可，例如：\n\n```\nlink: \u003C\u002Fmain.37d69167.css>; as=style; rel=preload, \u003C\u002Fmain.f06ad8b3.css>; as=style; rel=preload\n```\n\n此处比较科学的做法应该是使用一个中间件，在返回内容之前，根据要返回的HTML内容来处理preload header。\n\n因为我主要处理静态html文件，又用的koa，所以将主要逻辑放在了koa-static的`setHeaders`函数中。`setHeaders`主要用于在返回静态文件前设置自定义的header，刚好和server push的场景相符。\n\n\u003C!-- more -->\n\n主要逻辑：\n\n1. 读取html文件，使用正则表达式匹配出css和js文件的路径（如果有图片也可以一起）\n2. 将这些资源拼接成preload header的值\n\n```javascript\nconst htmlContent = fs.readFileSync(path, 'utf8');\nconst styleRegExp = \u002F\u003Clink(?:.*?)href=['\"]?([\\w\\.\u002F]+\\.css)['\"]?\u002Fg\nlet currentStyle;\nconst styleList = [];\nwhile(currentStyle = styleRegExp.exec(htmlContent)){\n    styleList.push(currentStyle[1]);\n\n}\nconst scriptRegExp = \u002F\u003Cscript(?:.*?)src=['\"]?([\\w\\.\u002F]+\\.js)['\"]?\u002Fg\nlet currentScript;\nconst scriptList = [];\nwhile(currentScript = scriptRegExp.exec(htmlContent)){\n    scriptList.push(currentScript[1]);\n}\nlet link = styleList.map((styleFile) => {\n    return `\u003C${styleFile}>; as=style; rel=preload`;\n}).join(', ');\nlink += ', ' + scriptList.map((scriptFile) => {\n    return `\u003C${scriptFile}>; as=script; rel=preload`;\n}).join(', ');\ncache[path] = link;\nres.setHeader('Link', link);\n```\n\n这样就可以实现http\u002F2 server push了。\n\n## 有缓存不再推送\n\n上面两步都超简单，网上的教程满天飞。这一个标题“有缓存不再推送”内容才是促成本文的原因。\n\n回到http\u002F2 server push的原理，浏览器访问`index.html`时，server除了返回html，还将css \u002F js \u002F image也一并推送回来，这样浏览器接受完之后，就不用再单独请求一次，从而加快页面的加载。\n\n但是这里有一个矛盾，如果我们的静态资源是有长缓存的，下一次请求的时候该推送还是不该推送呢？如果推送，则相当于是忽略了缓存，白白浪费带宽。\n\n到目前为止，这些仍然是网上文章中的主要结论。于是我就验证了一下，有缓存时是否真的会浪费带宽。于是我打开了`chrome:\u002F\u002Fnet-internals\u002F#http2`，然后找到了活跃的http\u002F2连接。在`Source Type`为`HTTP2 SESSION`那一栏中，可以看到详细的HTTP\u002F2通信过程。我截了一些图：\n\n首先是没有缓存的情况下，server push开启：\n\n1. 浏览器请求完html之后，发现了`PUSH_PROMISE`，按字面意思理解，也就是server承诺推荐这些资源\n    ![push promise](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F01.jpg)\n2. 接下来浏览器接受了这些推送的stream，把资源弄下来了\n    ![accept push](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F02.jpg)\n\n到这里一切正常。但是当资源有缓存时，再次请求，server push仍然开启的情况下：\n\n1. 浏览器收到PUSH_PROMISE，注意最后一行，`main.6e607578.js`的`stream_id`为`12`\n    ![push prmise stream id 12](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F03.jpg)\n2. 接下来搜索这个`stream_id=12`，找到一串看不懂的东西，看起来跟TCP窗口调整的逻辑很类似？\n    ![stream id 12](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F04.jpg)\n3. 再接着，罢工了……清楚地写着`Abandoned.`已放弃，应该是浏览器拒绝了这次推送，注意`stream_id`仍然是`12`\n    ![abandoned](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F05.jpg)\n\n也就是说，在有缓存的情况下，浏览器并不会傻乎乎地再接受推送。试验到这里后有点不可思议，于是又从两个方面做了验证。\n\n1. 网络带宽\n    这是`chrome:\u002F\u002Fnet-internals\u002F#timeline`的时间线，比较明显的有15个峰，分别是我发起的15次请求，前5次缓存有效，中间5次没有缓存，后5次缓存有效。可以明显看到，有缓存时带宽是比没缓存时低的，证实有缓存时push并不会真的发生。\n\n    ![abandoned](\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F05.jpg)\n2. 服务端网络IO\n    在有缓存的情况下，服务端网络IO大约每个请求0.2-0.4M，在无缓存的情况下，服务端网络IO大约每个请求2.2M-2.4M。同样证实有缓存的情况下，push并不会发生。\n\n## 有缓存不再推送\n\n其实有上面的结论后，不需要再做什么了。但是仍然怀疑这是不是哪一端实现的bug，要不然为什么大家都把这当作一个不可解决的缺陷呢？\n\n那么就假装我不知道上面这一段吧，还是需要自己根据是否有缓存控制是否开启server push。\n\n最容易想到的办法就是cookies了，将已推送过的资源url放入cookies中，下次再请求时进行对比，已有的资源就不再推送。\n\n但是这样的话，cookies会非常庞大。于是有人提出了使用BloomFilter来存放推送信息。\n\nBloomFilter是一个空间和时间复杂度都比较小的算法，主要用于快速进行“有损”存在性判定。所谓存在性判定就是给定一个key，确定它是否存在。而“有损”的意思则是指它并不100%精准。\n\n它的原理可以简单这么理解：首先放一个数组，接下来每一个需要检查的key都做一个hash，映射到这个数组中的某几个位置，如果这几个位置全部为`1`，则认为这个key存在，否则认为这个key不存在。\n\n考虑到server push的场景，即使不精准也不影响页面打开使用，因此它是适用的。HTTP server软件H2O就是使用了类似的算法。\n\n本来我也打算这么实现一版，但是后来转念一想，我就一个页面，3个资源，好像没必要这么麻烦。不如直接全部hash一把，hash匹配就认为有缓存，hash不匹配就认为没缓存，全部重新推送一遍。\n\n于是就有了类似这样的代码：\n\n```javascript\nlet pushHash = md5(cache[path]);\nif(!cookie || cookie !== pushHash){\n    res.setHeader('Link', cache[path]);\n    res.setHeader('Set-Cookie', 'push=' + pushHash);\n}\n```\n\n简单粗暴有效。\n\n参考：\n\n- https:\u002F\u002Fimququ.com\u002Fpost\u002Fcache-aware-server-push-in-h2o.html\n- https:\u002F\u002Fwww.keakon.net\u002F2018\u002F03\u002F07\u002FNGINX%E6%94%AF%E6%8C%81HTTP\u002F2serverpush%E4%BA%86\n",null,0,"2026-08-28 04:37:17","published","\u003Cp>一看这标题就是不准备好好写的。对的，最近特别忙，只能简单记录一下折腾的东西。\u003C\u002Fp>\n\u003Ch2>Nginx开启server push\u003C\u002Fh2>\n\u003Col>\n\u003Cli>升级nginx到1.13.9或以上版本（注意1.13.6修改http\u002F2的实现，与一些旧版本客户端不兼容，比如旧版Android okhttp）\u003C\u002Fli>\n\u003Cli>nginx配置中加上\u003Ccode>http2_push_preload on\u003C\u002Fcode>，表示使用preload header来作为server push标识\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>Node开启server push\u003C\u002Fh2>\n\u003Cp>Node处理文件内容时加上preload header即可，例如：\u003C\u002Fp>\n\u003Cpre class=\"shiki\" style=\"background-color:#121212;color:#dbd7caee\" tabindex=\"0\">\u003Ccode>link: &lt;\u002Fmain.37d69167.css&gt;; as=style; rel=preload, &lt;\u002Fmain.f06ad8b3.css&gt;; as=style; rel=preload\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>此处比较科学的做法应该是使用一个中间件，在返回内容之前，根据要返回的HTML内容来处理preload header。\u003C\u002Fp>\n\u003Cp>因为我主要处理静态html文件，又用的koa，所以将主要逻辑放在了koa-static的\u003Ccode>setHeaders\u003C\u002Fcode>函数中。\u003Ccode>setHeaders\u003C\u002Fcode>主要用于在返回静态文件前设置自定义的header，刚好和server push的场景相符。\u003C\u002Fp>\n\u003Cp>主要逻辑：\u003C\u002Fp>\n\u003Col>\n\u003Cli>读取html文件，使用正则表达式匹配出css和js文件的路径（如果有图片也可以一起）\u003C\u002Fli>\n\u003Cli>将这些资源拼接成preload header的值\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cpre>\u003Ccode>const htmlContent = fs.readFileSync(path, 'utf8');\nconst styleRegExp = \u002F&lt;link(?:.*?)href=['&quot;]?([\\w\\.\u002F]+\\.css)['&quot;]?\u002Fg\nlet currentStyle;\nconst styleList = [];\nwhile(currentStyle = styleRegExp.exec(htmlContent)){\n    styleList.push(currentStyle[1]);\n\n}\nconst scriptRegExp = \u002F&lt;script(?:.*?)src=['&quot;]?([\\w\\.\u002F]+\\.js)['&quot;]?\u002Fg\nlet currentScript;\nconst scriptList = [];\nwhile(currentScript = scriptRegExp.exec(htmlContent)){\n    scriptList.push(currentScript[1]);\n}\nlet link = styleList.map((styleFile) =&gt; {\n    return `&lt;${styleFile}&gt;; as=style; rel=preload`;\n}).join(', ');\nlink += ', ' + scriptList.map((scriptFile) =&gt; {\n    return `&lt;${scriptFile}&gt;; as=script; rel=preload`;\n}).join(', ');\ncache[path] = link;\nres.setHeader('Link', link);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>这样就可以实现http\u002F2 server push了。\u003C\u002Fp>\n\u003Ch2>有缓存不再推送\u003C\u002Fh2>\n\u003Cp>上面两步都超简单，网上的教程满天飞。这一个标题“有缓存不再推送”内容才是促成本文的原因。\u003C\u002Fp>\n\u003Cp>回到http\u002F2 server push的原理，浏览器访问\u003Ccode>index.html\u003C\u002Fcode>时，server除了返回html，还将css \u002F js \u002F image也一并推送回来，这样浏览器接受完之后，就不用再单独请求一次，从而加快页面的加载。\u003C\u002Fp>\n\u003Cp>但是这里有一个矛盾，如果我们的静态资源是有长缓存的，下一次请求的时候该推送还是不该推送呢？如果推送，则相当于是忽略了缓存，白白浪费带宽。\u003C\u002Fp>\n\u003Cp>到目前为止，这些仍然是网上文章中的主要结论。于是我就验证了一下，有缓存时是否真的会浪费带宽。于是我打开了\u003Ccode>chrome:\u002F\u002Fnet-internals\u002F#http2\u003C\u002Fcode>，然后找到了活跃的http\u002F2连接。在\u003Ccode>Source Type\u003C\u002Fcode>为\u003Ccode>HTTP2 SESSION\u003C\u002Fcode>那一栏中，可以看到详细的HTTP\u002F2通信过程。我截了一些图：\u003C\u002Fp>\n\u003Cp>首先是没有缓存的情况下，server push开启：\u003C\u002Fp>\n\u003Col>\n\u003Cli>浏览器请求完html之后，发现了\u003Ccode>PUSH_PROMISE\u003C\u002Fcode>，按字面意思理解，也就是server承诺推荐这些资源\n\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F01.jpg\" alt=\"push promise\">\u003C\u002Fli>\n\u003Cli>接下来浏览器接受了这些推送的stream，把资源弄下来了\n\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F02.jpg\" alt=\"accept push\">\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>到这里一切正常。但是当资源有缓存时，再次请求，server push仍然开启的情况下：\u003C\u002Fp>\n\u003Col>\n\u003Cli>浏览器收到PUSH_PROMISE，注意最后一行，\u003Ccode>main.6e607578.js\u003C\u002Fcode>的\u003Ccode>stream_id\u003C\u002Fcode>为\u003Ccode>12\u003C\u002Fcode>\n\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F03.jpg\" alt=\"push prmise stream id 12\">\u003C\u002Fli>\n\u003Cli>接下来搜索这个\u003Ccode>stream_id=12\u003C\u002Fcode>，找到一串看不懂的东西，看起来跟TCP窗口调整的逻辑很类似？\n\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F04.jpg\" alt=\"stream id 12\">\u003C\u002Fli>\n\u003Cli>再接着，罢工了……清楚地写着\u003Ccode>Abandoned.\u003C\u002Fcode>已放弃，应该是浏览器拒绝了这次推送，注意\u003Ccode>stream_id\u003C\u002Fcode>仍然是\u003Ccode>12\u003C\u002Fcode>\n\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F05.jpg\" alt=\"abandoned\">\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Cp>也就是说，在有缓存的情况下，浏览器并不会傻乎乎地再接受推送。试验到这里后有点不可思议，于是又从两个方面做了验证。\u003C\u002Fp>\n\u003Col>\n\u003Cli>\n\u003Cp>网络带宽\n这是\u003Ccode>chrome:\u002F\u002Fnet-internals\u002F#timeline\u003C\u002Fcode>的时间线，比较明显的有15个峰，分别是我发起的15次请求，前5次缓存有效，中间5次没有缓存，后5次缓存有效。可以明显看到，有缓存时带宽是比没缓存时低的，证实有缓存时push并不会真的发生。\u003C\u002Fp>\n\u003Cp>\u003Cimg src=\"https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F05.jpg\" alt=\"abandoned\">\u003C\u002Fp>\n\u003C\u002Fli>\n\u003Cli>\n\u003Cp>服务端网络IO\n在有缓存的情况下，服务端网络IO大约每个请求0.2-0.4M，在无缓存的情况下，服务端网络IO大约每个请求2.2M-2.4M。同样证实有缓存的情况下，push并不会发生。\u003C\u002Fp>\n\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>有缓存不再推送\u003C\u002Fh2>\n\u003Cp>其实有上面的结论后，不需要再做什么了。但是仍然怀疑这是不是哪一端实现的bug，要不然为什么大家都把这当作一个不可解决的缺陷呢？\u003C\u002Fp>\n\u003Cp>那么就假装我不知道上面这一段吧，还是需要自己根据是否有缓存控制是否开启server push。\u003C\u002Fp>\n\u003Cp>最容易想到的办法就是cookies了，将已推送过的资源url放入cookies中，下次再请求时进行对比，已有的资源就不再推送。\u003C\u002Fp>\n\u003Cp>但是这样的话，cookies会非常庞大。于是有人提出了使用BloomFilter来存放推送信息。\u003C\u002Fp>\n\u003Cp>BloomFilter是一个空间和时间复杂度都比较小的算法，主要用于快速进行“有损”存在性判定。所谓存在性判定就是给定一个key，确定它是否存在。而“有损”的意思则是指它并不100%精准。\u003C\u002Fp>\n\u003Cp>它的原理可以简单这么理解：首先放一个数组，接下来每一个需要检查的key都做一个hash，映射到这个数组中的某几个位置，如果这几个位置全部为\u003Ccode>1\u003C\u002Fcode>，则认为这个key存在，否则认为这个key不存在。\u003C\u002Fp>\n\u003Cp>考虑到server push的场景，即使不精准也不影响页面打开使用，因此它是适用的。HTTP server软件H2O就是使用了类似的算法。\u003C\u002Fp>\n\u003Cp>本来我也打算这么实现一版，但是后来转念一想，我就一个页面，3个资源，好像没必要这么麻烦。不如直接全部hash一把，hash匹配就认为有缓存，hash不匹配就认为没缓存，全部重新推送一遍。\u003C\u002Fp>\n\u003Cp>于是就有了类似这样的代码：\u003C\u002Fp>\n\u003Cpre>\u003Ccode>let pushHash = md5(cache[path]);\nif(!cookie || cookie !== pushHash){\n    res.setHeader('Link', cache[path]);\n    res.setHeader('Set-Cookie', 'push=' + pushHash);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>简单粗暴有效。\u003C\u002Fp>\n\u003Cp>参考：\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fimququ.com\u002Fpost\u002Fcache-aware-server-push-in-h2o.html\">https:\u002F\u002Fimququ.com\u002Fpost\u002Fcache-aware-server-push-in-h2o.html\u003C\u002Fa>\u003C\u002Fli>\n\u003Cli>\u003Ca href=\"https:\u002F\u002Fwww.keakon.net\u002F2018\u002F03\u002F07\u002FNGINX%E6%94%AF%E6%8C%81HTTP\u002F2serverpush%E4%BA%86\">https:\u002F\u002Fwww.keakon.net\u002F2018\u002F03\u002F07\u002FNGINX支持HTTP\u002F2serverpush了\u003C\u002Fa>\u003C\u002Fli>\n\u003C\u002Ful>\n","\u003Cp>一看这标题就是不准备好好写的。对的，最近特别忙，只能简单记录一下折腾的东西。\u003C\u002Fp>\n\u003Ch2>Nginx开启server push\u003C\u002Fh2>\n\u003Col>\n\u003Cli>升级nginx到1.13.9或以上版本（注意1.13.6修改http\u002F2的实现，与一些旧版本客户端不兼容，比如旧版Android okhttp）\u003C\u002Fli>\n\u003Cli>nginx配置中加上\u003Ccode>http2_push_preload on\u003C\u002Fcode>，表示使用preload header来作为server push标识\u003C\u002Fli>\n\u003C\u002Fol>\n\u003Ch2>Node开启server push\u003C\u002Fh2>\n\u003Cp>Node处理文件内容时加上preload header即可，例如：\u003C\u002Fp>\n\u003Cpre class=\"shiki\" style=\"background-color:#121212;color:#dbd7caee\" tabindex=\"0\">\u003Ccode>link: &lt;\u002Fmain.37d69167.css&gt;; as=style; rel=preload, &lt;\u002Fmain.f06ad8b3.css&gt;; as=style; rel=preload\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>此处比较科学的做法应该是使用一个中间件，在返回内容之前，根据要返回的HTML内容来处理preload header。\u003C\u002Fp>\n\u003Cp>因为我主要处理静态html文件，又用的koa，所以将主要逻辑放在了koa-static的\u003Ccode>setHeaders\u003C\u002Fcode>函数中。\u003Ccode>setHeaders\u003C\u002Fcode>主要用于在返回静态文件前设置自定义的header，刚好和server push的场景相符。\u003C\u002Fp>\n","https:\u002F\u002Fassets.toobug.net\u002Fassets\u002Fweb\u002F2018\u002Fnginx-koa-use-http2-server-push\u002F01.jpg",{"total":16,"totalRoots":16,"comments":23,"pv":16},[]]