概要
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さんご本人よりはてブコメントいただきました。
私なりの考察ですが
- 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">