ぼちぼち日記

おそらくプロトコルネタを書いていることが多いんじゃないかと思います。

Node.js で Hello SPDY を作る

SPDY が流行っている

SPDY が流行っています。当初 Chrome だけ使えていたのが FireFox のサポートするようになり、Google からの apache モジュールの提供や nginx のサポート予定などここ最近で SPDY に関する話題が急に増えています。Node.js では昨年既に node-spdy のモジュールが提供されていて、比較的早い段階から使えるようになっていました。(ただし node-0.7系が必要です。) これはおそらく Node は tls/zlib モジュールが使えるので比較的 SPDY の実装するのが楽だからじゃないかと個人的に見てまして、まぁ流行に乗って、今回まずは簡単・単純 Hello SPDY を独自に作ってみました。やっぱり新しいプロトコルを使ってみるのは新鮮ですね。

そもそも SPDY がどんな技術でどんなメリットがあるかなどは、それを書くと長くなっちゃいますし時間も足りないので今回はスキップしておきます。
ただ SPDY がどんな仕組みで動作しているかは、別にスライドを突貫で作りました。
SPDYの中身を見てみる
あまり説明を書いていないスライドですが、これに従って話を進めます。

SPDY の仕組み

スライドにあるよう SPDY では下図の通り2種類のフレーム(Data FrameとControl Frame)を使ってデータのやり取りをします。

Control Frame は、用途別に10種類ぐらいのタイプに分かれていて、クライアントーサーバ間のTLSソケット通信でこの Control Frame を交換しあいます。この一連の Control Frame のやり取りを Stream と呼びます。Stream には ID番号が付いていてサーバから生成されたら偶数・クライアントから生成されたものは奇数になっています。1つのTCPセッションを保持しながら複数のストリームのオープン・クローズを随時行います。これによって SPDY はネットワーク接続のオープン・クローズのオーバーヘッドをなくし、HTTP処理が高速に行えるようになるわけです。
単純なストリームのフローはこんな感じです。

実際に https://google.com/ 宛に接続した SPDY がどんなストリームフローになるのか、Chrome から SPDY サーバに接続するとどんなストリームフローができるのかは先のスライドを参照して下さい。

尚、今回 Hello SPDY サーバを作るのに Spdylay - SPDY C Library に大変お世話になりました。SPDY のリファレンス実装として有用で、 SPDY を使ったアプリの試験やデバッグには欠かせないツールだと思います。本当に有用なライブラリーを公開していただきありがとうございます。ここにお礼を述べさせていただきます。

Node.js で Hello SPDY を作る

で、今回 Hello SPDY サーバは、この簡単なフローの一部「SYN_STREAM を受け取り、SYN_REPLYとDataを返す」ところの部分だけを作ってみました。
(本当は GOAWAY まで必要でしょうが、tls ソケットをクローズして無理やり終わらせてます。)
SPDY は Frame フォーマットが異なるVer.2 と 3 が存在しています。現在主に利用されているのは Ver.2 ですが、今後 Ver.3 の導入が進むと思われるので 今回 Ver.3 で実装しました。(注: 現在 Chrome で SPDY Ver.3 を利用するには about:flags で利用を許可する設定が必要です。)

Hello SPDY だけ表示するSPDYサーバのサンプルコードを gist にあげてます。(できるだけ短いコードにするためエラー処理はわざと省いてあります。)
https://gist.github.com/2716248
また SDPY では zlib でデータを圧縮する時に利用する辞書が必要ですが、node-spdy のファイルを利用してください。(これは仕様書にベタに記載されています。)
https://github.com/indutny/node-spdy/blob/spdy-v3/lib/spdy/protocol/v3/dictionary.js
SSLの証明書・鍵ファイルはオレオレでも大丈夫なので各自準備してください。このサーバを起動して Chrome でアクセスすると

のように Hello SPDY の文字列と青○で囲んだSPDY利用表示のイナズマが見えます。(SPDYを利用していることは SPDY indicator 拡張を使うとこのように見えます。window.chrome.loadTimes().wasFetchedViaSpdy 変数をチェックしているようですね。)

Hello SPDY 実装の流れ

コードを全部解説するのはとても長くなるので実装の流れとポイントを箇条書きで書いてみます。

tls サーバを作る
  • オプションに NPNProtocols: ['spdy/3'] を指定する。(spdy/3 以外も許可する場合はそのプロトコルを列挙する)
  • TLSのハンドシェイクでクライアント側が spdy/3 に対応していれば cleartext.npnProtocol に spdy/3 が入ってくるのでそれをチェック
  • クライアントから送られたデータをバッファに溜めていきます。
  • 8バイトを超えたらフレームヘッダが取得できるので length フィールドを読み取ります。これで送られてくるフレームサイズが取得できます。
  • フレームサイズ分データを受信したら SPDY のプロトコル表に従って各フィールド値を取得します。今回簡単にするため最初は SYN_STREAM が来ると決め打ちしてます。
SYN_STREAM の読み込みと解析
  • SYN_STREAM 内のデータは以下の順番で並んでいますので BufferのAPIを使って読み込んでいきます。

  • グレーの領域に圧縮されたリクエストヘッダが入っているのでこれを解凍します。(inflate)
  • Node の zlib モジュールを使って spdy/3 の辞書を持つ inflate オブジェクトを生成します。これに圧縮データを書き込むと解凍したデータが返ってきます。
  • 解凍されたデータには Name:Value の数と Name長、Name値、Value長、Value値が並んでいるので、その順番で読み込み SYN_STREAM の解析が終了しました。
SYN_REPLY の作成と送信

Hello SPDY サーバなのでどんなリクエストが来ようと "Hello SPDY" を返せばいいだけです。なのでそれ用のHTTPレスポンスヘッダを含む SYN_REPLY ストリームを作成してクライアントに送信してあげます。SYN_REPLY の書式は次の通りです。

各データは下の通りに設定します。Stream-ID は SYN_STREAM で受けたものと同一なものにします。

  var syn_reply = new SPDYStream({control: true,
                                  version: syn_stream.version,
                                  type: 0x02,  // SYN_REPLY
                                  flags: 0x00,
                                  streamid: syn_stream.streamid
                                 });

そしてHTTPレスポンスヘッダです。HTTPのステータス・バージョン・スキームといった必須のフィールドは :(コロン) で始まる仕様になっていますのでそれに合わせます。(ヘッダは全部小文字です。)

  var headers = [[':status', '200 OK'],
                 [':version', 'HTTP/1.1'],
                 ['conent-length', word.length + ''],
                 ['content-type', 'text/html'],
                 ['date', (new Date()).toUTCString()]];

これを今度は 「Name:Value の数と Name長、Name値、Value長、Value値」の並びにして圧縮(deflate) します。圧縮には指定された辞書が必要です。
ここで注意すべきことは deflate.flush() を使うことです。(仕様書には圧縮フレームの間にSYNC_FLUSHを使うと”こっそり”書いてあります。)私はこれを見逃してクライアント側で全然データの解凍してもらえず数日悩みました。後はBuffer型データに並べて書き込み cleartext に write() してあげればOKです。Nodeじゃこの辺のAPIはとっても充実しています。

Dataフレームの作成と送信

最後にHTMLレスポンスデータの送信です。

この形にして cleartext に書き込んで終わりです。ボディデータは圧縮しなくてもいいんですよね。FLAG にも FIN を入れるのを忘れずに。

  var data = new SPDYStream({control: false,
                             streamid: syn_stream.streamid,
                             flags: 0x01, // FLAG_FIN
                             data: word
                            })

終わりに

本当はもっと短いコードで実装できるのかと見込んでたんですが、そこそこの量になりました。Buffer型データの操作がオンパレードでブラウザ上のJSにはない Node ならではの感じです。本当は GOAWAY/PING/RST_STREAM 等のストリーム制御と HEADERS/SETTINGS/WINDOW_UPDATE 等のデータのやり取りロジック等が必要です。SPDY ならでは Server PUSH (サーバからブラウザーで先読みキャッシュの強制送信)機能など面白いんだけど、これからぼちぼち作っていこうかと思います。サーバ側は node-spdy があるので、クライアント側でも作れたらなと思っています。