daichi.dev

HTTP/2でXMLHttpRequest Level2

@daichirata

モバイルなどのブラウザからサーバーに何らかのデータを定期的に送信したい場合、出来るだけパケットやコネクション数を減らしたい。今であればHTTP/2を使うのが最も適していると思うのだけど、Javascriptから送信するのでXHRでリクエストを投げることになる。imageやcssなどのリソースは当然HTTP/2でリクエストされると思うけど、HTTP/1.1でアクセスしたページからXHRでリクエストを投げた場合や、CORSなリクエストに対しての通信の場合にもちゃんとHTTP/2でアクセスされているかを確認するため、以下の2パターンを実際に動かしてみる。

Setup

まず初めに、HTTP/2に対応しているH2OをDockerを使って構築する。

ファイル一式をdaichirata/htt2_xhrに置いているので、 docker-machineを使っていてipが192.168.99.100であればそのまま使えると思う。

FROM buildpack-deps

RUN apt-get update && \
    apt-get install -y cmake && \
    rm -rf /var/lib/apt/lists/*

RUN git clone --recursive https://github.com/h2o/h2o --depth 1 && \
    cd h2o && \
    cmake . && \
    make h2o

WORKDIR /h2o
CMD ./h2o -c /h2o_conf/h2o.conf
access-log: /dev/stdout

listen:
  port: 80
listen:
  port: 443
  ssl:
    certificate-file: /h2o_conf/server.crt
    key-file: /h2o_conf/server.key
hosts:
  "*:80":
    paths:
      /:
        file.dir: /h2o_conf/doc_root
  "*:443":
    header.add: "Access-Control-Allow-Origin: *"
    paths:
      /:
        file.dir: /h2o_conf/doc_root

証明書にはオレオレ証明書を使うが、XHRが失敗するのでFQDNの所はちゃんとした値を入れる。 今回はdocker-machineとxip.ioを使うのでhttps://192.168.99.100.xip.ioに対しての証明書を発行する。

生成されたRoot証明書をテストのためにkeychainに登録する。終わったらちゃんと削除すること。

$ openssl genrsa 2048 > server.key
$ openssl req -new -key server.key > server.csr
$ openssl x509 -days 3650 -req -signkey server.key < server.csr > server.crt

ブラウザからはトップページに対してGETパラメーター付きでリクエストを送信する

document.addEventListener('DOMContentLoaded', function(){
  var xhr = new XMLHttpRequest();
  xhr.open('get', 'https://192.168.99.100.xip.io/?hoge=fuga');
  xhr.send();
}, false);

XHR on HTTP/2

XHRがHTTP/2で通信しているかどうかを確認するには、ChromeのDeveloper Consoleを使うかh2oのアクセスログを見ればいい。Developer ConsoleのNetworkのタブにProtocolが表示されていない場合は、NameとかMethodの上で右クリックすると表示できる。

HTTP -> HTTP/2

同一ドメインに対してHTTP -> HTTP/2のXHR。特に問題なくh2でリクエストが飛んでいる事が分かる。

HTTP -> CORS HTTP/2

別ドメイン(192.168.99.100 -> 192.168.99.100.xip.io)に対してのHTTP -> HTTP/2のXHR。こちらも問題なくh2でリクエストが飛んでいる。

WebSocket vs HTTP/2

パケットやコネクション数を減らしたいモチベーションであれば、WebSocketでも同様の事が出来そうに思える。だけど今回のケースでは以下の点でWebSocketはマッチしない様に思う。

1. WebSocketを解釈できるアプリケーションサーバーを書かなければいけない

何らかのデータを送信するだけでレスポンスを受け取る必要がない場合、フロントのAPIサーバーではリクエストのバリデーションとDB・Fluentd・ファイルのいずれかに出力する位の実装にしておいて、集計や分析にはバックエンドの別のミドルウェアを使う事になると思う。WebSocketはあくまでもサーバー側でイベントが発生したり、クライアントにデータをPushする様なアプリにこそ向いているので、今回の件ではオーバースペックに思う。また、既存のHTTPのセマンティクスに乗っかっておくほうが負荷分散や運用上の資産の流用等の点でアドバンテージがあるだろう。

2. モバイルの場合、平文のWebSocketが通らないことがある

HTTP/2のh2c(平文のHTTP/2)も同じ事が言えるが、httpでWebSocketのネゴシエーションを行う場合、初めにクライアントとサーバー間でHTTP/1.1のUpgradeヘッダーのやり取りが必要になる。モバイルにはOperaMaxのような帯域の節約の為プロキシを挟んで通信している事があり、そのプロキシがUpgradeヘッダーを強制的に書き換えてしまう為ネゴシエーションに失敗することがある。勿論TLSで通信すれば問題ないけど、そうなってしまうと1で上げた点からもHTTP/2を使うほうが良いように思う。

WebSocket不要説?

余談ではあるが、HTTP/2があればWebSocketはいらない子なのか、というと決してそういうわけではない。HTTP/2はWebSocketを置き換えるために生まれたわけではなく、Webを早く、軽くする為に生まれたSPDYを標準化させたプロトコルだ。あくまでもHTTP/1.1ベースのプロトコルなのでServerPush APIはHTTP/1.1ベースのアプリケーションから使用するには難しい部分がある。

一方、WebSocketはリアルタイムWebを実現する為にAjax・Cometを経て生まれてきたという背景がある。強力な双方向通信を持っていて、先程も上げたようにサーバー側でイベントが発生するような場合、他のクライアントのイベントを通知する必要がある場合、リアルタイム性を求められる場合のアプリケーションを低レイテンシーで実現する為のプロトコルなのである。

HTTP/2便利

というわけで、HTTP/2最大の長所といっていいかもしれない既存のリソース変更なしに透過的にHTTP/2を適用することが出来た。そしてそれは勿論XHRでも適用されていることが確認できた。実際に業務とかで使うことはまだそんなに無いのであまり実感していなかったけど、やっぱりこれは凄い事だなー。ただ、逆に言うとサーバー・ブラウザ対応含めここまでやらないとこの時代に新しいプロトコルを広めていくっていうのは難しいという事なんだろう。HTTP/2が使えないクライアントであればこれまで通りのHTTP/1.1にフォールバックするので、そろそろ実際の案件に適用してみたい今日この頃。