LoginSignup
14

More than 5 years have passed since last update.

PSR-7対応 HTTP/2サーバプッシュ用ミドルウェア

Last updated at Posted at 2015-12-05

概要

HTTP/2には、HTML内に含まれるリソースに対し、クライアントが次にリクエストを送ってくることを見越して、リクエスト無しにサーバ側から能動的に送るサーバプッシュという機能があります。H2Oやnghttp2といったWebサーバはプリロード用のLinkヘッダを付与することでサーバプッシュを行ってくれるとのことなので、PSR-7対応のミドルウェアを書いてみました…

  • AdventCalendarの人たち怖いので普通に野良投稿
  • こういうのってmod_mrubyでやったほうが良いの?

ミドルウェア定義

Middleware/AutoLinker.php
<?php

namespace mpyw\SampleApp\Middleware;

use \Psr\Http\Message\ServerRequestInterface;
use \Psr\Http\Message\ResponseInterface;

class AutoLinker {

    /**
     * インスタンスをそのままミドルウェアとして渡すことを想定
     *
     * @access public
     * @param  Psr\Http\Message\ServerRequestInterface $request  PSR7 request
     * @param  Psr\Http\Message\ResponseInterface      $response PSR7 response
     * @param  callable                                $next     Next middleware
     * @return Psr\Http\Message\ResponseInterface
     */
    public function __invoke(
        ServerRequestInterface $request,
        ResponseInterface $response,
        callable $next
    ) {
        $response = $next($request, $response);
        $server = $request->getServerParams();
        if (isset($server['SERVER_PROTOCOL']) && $server['SERVER_PROTOCOL'] === 'HTTP/2') {
            $response = static::withPreloadHeaders($response);
        }
        return $response;
    }

    /**
     * HTMLタグに応じたプリロードヘッダを付加する
     *
     * @access protected
     * @static
     * @param  Psr\Http\Message\ResponseInterface $response PSR7 response
     * @return Psr\Http\Message\ResponseInterface
     */
    protected static function withPreloadHeaders(ResponseInterface $response) {
        $dom = new \DOMDocument;
        $response->getBody()->rewind();
        @$dom->loadHTML($response->getBody()->getContents());
        $xpath = new \DOMXPath($dom);
        $selectors = [
            'style' => '//link[@rel="stylesheet"]/@href',
            'script' => '//script/@src',
            'image' => '//img/@src',
        ];
        foreach ($selectors as $as => $selector) {
            foreach ($xpath->query($selector) as $node) {
                $value = "<{$node->nodeValue}>; rel=preload; as={$as}";
                $response = $response->withAddedHeader('Link', $value);
            }
        }
        return $response;
    }

}

使用例

index.php
<?php

namespace mpyw\SampleApp;

require 'vendor/autoload.php';

$container = new \Slim\Container;
$app = new \Slim\App($container);
$app->add(new Middleware\AutoLinker);
$app->get('/', function ($request, $response) {
    return $response->write('
        <!DOCTYPE html>
        <meta charset="UTF-8">
        <title>Example</title>
        <link rel="stylesheet" href="style.css">
        <img src="example.png">
        <script src="default.js"></script>
   ');
});

問題点

  • クライアント側がキャッシュを保持しているにも関わらず毎回サーバプッシュでコンテンツを送信するのは無駄がある。
  • (静的なサーバプッシュに関してはmod_mruby等でやればいいが)FastCGIを使用する場合で動的なコンテンツに関して、HTML生成に時間がかかるが一部のリソースをプッシュすることが確定している場合においても、全ての処理が終わるまでサーバがプッシュを実行することが出来ない。

HTTP/2の仕様が策定されたといえ、サーバプッシュに関してはもう少し、という印象でしょうか。

追記

H2O作者のkazuhoさんご本人よりはてブコメントいただきました。

スクリーンショット 2015-12-11 22.14.34.png

私なりの考察ですが

  • PHPではレスポンスボディを1バイト以上出力し始めるまでレスポンスヘッダはバッファリングされ続ける。
  • HTTP/2ではTransfer-Encoding: chunkedはサポートされていない。

ということを考慮すると、現在のPHPの構造上、実現は厳しい気がします…
(あらかじめ長めにContent-Lengthを確保しておく方法もあるが現実的ではない)

こんな感じに出来たらいいなぁという理想(HTTP/1.1風の書き方)
GET /example.php HTTP/2
Host: example.com
Link: </example.css>; rel=preload; as=style
Link: </example.js>; rel=preload; as=script
Link: </example.png>; rel=preload; as=image
[... あらかじめここまでH2Oに渡しておいて長時間の処理に入る ...]
Content-Length: xxx

<!DOCTYPE html>
<title>Very Heavy Response</title>
Sorry for stupid latency, but all the following resources are preloaded!
<link rel="stylesheet" href="/example.css">
<script src="/example.js"></script>
<img src="/example.png">

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14