Node の HTTP/2.0 モジュール
iij-http2 の実装苦労話
IIJ 大津 繁樹
2013年8月21日
第10回東京Node学園
自己紹介
• 株式会社インターネットイニシアティブ(IIJ)
プロダクト本部 戦略的開発部所属
• twitter: @jovi0608
• github: https://github.com/shigeki/
• ブログ: http://d.hatena.ne.jp/jovi0608/
• Node とか、HTML5とか、HTTP/2.0とか、流行そうな
技術の評価検証してます。
• 最近、HTTP/2.0仕様修正やNode.js のHTTP/2.0モ
ジュールの開発をしています。
ちょっとNodeの最新トピックス(PR5464)
var EventEmitter = require("events") ;
var emitter = new EventEmitter();
と書けるようになりました!(ただしv0.11.6~)
あと、ストリームについては、3か月前
こうでした。
http://www.slideshare.net/shigeki_ohtsu/stream2-kihon
もう時代は先に (v0.11.5~)
• old mode/new mode はなくなり flowing mode/paused mode に、
• stream1 + stream2 = streams3
• data イベント復活
• ただし以前のAPIと互換性はありますのでご安心を
今回のiij-http2は Streams3 を利用
さて本題へ、
Nodeを使って新しいプロトコル(HTTP/2.0)を実
装したというお話です。
(コードはまだ未公開です。テスト追加やらドキュメント整備やらリファクタやら・・・)
SPDY、HTTP/2.0について
http://www.iij.ad.jp/company/development/tech/activities/spdy/
これ読んでください。
テキストベースのプロトコルからバイナリープロトコルへ
年 月 トピック
2012年1月 IETF httpbis WGでHTTP/2.0の仕様検討開始することを決定
2012年11月 3つの候補案からSPDY仕様をベースにすることを決定
draft-00(SPDY/3仕様をそのまま)リリース
2013年1月 第1回中間会議(東京)
draft-01リリース(HTTPからのUpgrade方法を追加)
2013年4月 draft-02リリース(フレームフォーマット・タイプの大幅な変更)
2013年5月 draft-03リリース(中間会議に向けて修正点の整理・まとめ)
2013年6月 第2回中間会議(サンフランシスコ)
2候補案を合わせたヘッダ圧縮仕様の採用を決定
2013年7月 draft-04リリース(最初の実装仕様)
2013年8月 第3回中間会議(ハンブルグ)
最初のHTTP/2.0相互接続試験を実施
draft-05リリース(接続試験結果を反映)
これまでの HTTP/2.0 仕様策定作業の主な歩み
今ここ、
iij-http2 を持ち込んだ
2014年春に仕様化完了を目指す
相互試験の細かい話は、
http://www.slideshare.net/shigeki_ohtsu/httpbis-interim-http20-25197160
あと、今朝 「HTTP/2.0 Draft 04 日本語訳」も公開されました。GJ!
http://summerwind.jp/docs/draft-ietf-httpbis-http2-04/
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
Length(16) Type(8) Flags(8)
R Stream Identifier(31)
Frame Payload
HTTP/2.0 Frame Format
Type Name 役割
0x0 DATA リクエスト・レスポンスボディ
0x1 HEADERS リクエスト・レスポンスヘッダ
0x2 PRIORITY レスポンスの優先度設定
0x3 RST_STREAM ストリームのリセット
0x4 SETTINGS 設定情報
0x5 PUSH_PROMISE サーバプッシュの予約
0x6 PING 生死確認
0x7 GOAWAY 終了宣言
0x9 WINDOW_UPDATE フロー制御ウィンドウの更新
0x8は欠番
重要なのは
この二つ
最初の8バイト分を
見れば大丈夫
HTTP/2.0のフレー
ムヘッダは8バイト
iij-http2の設計方針
• 7/8に draft-04がリリース。実装期間は約4週間弱。
• node-spdyはクライアントがない。spdy/3フレーム
フォーマットに大きく依存した実装なのでヤメ。
• Chromeのspdyスタック(Visitorパターン)を参考
– コネクション生成→フレーム解析→コールバック→ストリー
ム処理
• Nodeのhttpモジュールを最大限に流用
(Outgoing/Incomingクラスを利用、後で後悔)
• HTTP/2.0サーバの初期ハンドシェイク(後述)
は、とりあえずTLS+NPNとDirectの2種だけに。
iij-http2 の基本構成
(HTTPバインディング部分は除く)
Connection
(TCP接続)
OnDATA(frame)
OnHEADERS(frame)
・・・・
onWINDOW_UPDATE(frame)
Frame
HEADERSDATA WINDOW_UPDATE・・・・
Stream
(多重化)
stream_id
priority
_state
ストリー
ム生成
ソケットデータを
フレーム解析&
コールバック
ストリーム状態
遷移を管理
WritableStreamを
利用
HTTP Server のおさらい(v0.11)
TCP socket サーバに
HTTP/HTTPS サーバが
ぶらさがっているのよ
HTTP Serverのコア
lib/_http_server.js (v0.11)
function Server(requestListener) {
(中略)
this.addListener('connection', connectionListener);
}
util.inherits(Server, net.Server);
function connectionListener(socket) {
// とんでもない処理
// (おそらくNode.jsコアの中で最難関の一つ)
}
HTTP/2.0向けにconnectionListener をどう書くかがキモ
HTTPSサーバと共通化しているのでSSL化は楽
Hello World! with HTTP/1.1
var http = require(‘http’);
var server = http.createServer(function(req, res) {
res.writeHead(200, {‘content-type’: ‘text/plain’});
res.end(‘Hello World!’);
});
server.listen(8080);
目標:HTTP/2.0でこれと同じ処理ができるようにしよう
HTTP/2.0の接続方法(第1段階目)
パターンは3種類
あらかじめサーバがHTTP/2.0対応とわかって
いる場合、直接第2段階の接続方法を行う。
HTTP/1.1の接続後 Upgradeヘッダを使って、
HTTP/2.0 に接続をアップグレードする。
TLS接続時にALPN拡張フィールドを利用して
HTTP/2.0に接続を行う。(1) TLS + ALPN
(2) HTTP Upgrade
(3) Direct接続
opensslが当時非
対応だったので
NPNを利用
WebSocketと一緒
今回は未実装
DNSやAlternate-Protocolを想定
実装が一番簡単
iij-http2は、サーバは 1 と 3 、クライアントは 1,2,3 を実装
HTTP/2.0の接続方法(第2段階目)
505249202a20485454502f322e300d0a0d0a534d0d0a0d0a
PRI * HTTP/2.0¥r¥n
¥r¥n
SM¥r¥n
¥r¥n
クライアントから謎の24byteのマジックコードをサー
バに送り、最終チェックする。
SETTINGS(初期設定値の交換)
HEADERS(HTTPレスポンス)
HEADERS(HTTPリクエスト)
iij-http2コネクションリスナ実装概要
function connectionListener(socket) {
var self = this;
var connection = new Connection(socket, 'http_server', {});
socket.pipe(connection);
connection.on('Incoming', function(req) {
var res = new ServerResponse(req, req._stream);
res.assignSocket(connection);
self.emit('request', req, res);
});
}
pipeでソケットデータを渡す
リクエストヘッダの処理完了
Hello Worldのコールバックを emit
レスポンスオブジェクト生成
Connectionクラス
function Connection(socket, endpoint_type, opts) {
node_stream.Writable.call(this, opts);
(中略)
this.on('ConnectionHeader', OnConnectionHeader);
this.on('FrameHeader', OnFrameHeader);
this.on('FramePayload', OnFramePayload);
this.on('DATA', OnDATA);
this.on('HEADERS', OnHEADERS);
this.on('PRIORITY', OnPRIORITY);
this.on('RST_STREAM', OnRST_STREAM);
this.on('SETTINGS', OnSETTINGS);
this.on('PUSH_PROMISE', OnPUSH_PROMISE);
this.on('PING', OnPING);
this.on('GOAWAY', OnGOAWAY);
this.on('WINDOW_UPDATE', OnWINDOW_UPDATE);
}
結局バカでかいクラスに。 Chrome も同じでした。
初期接続処理
Connection.prototype._write = function(chunk, encoding, cb) {
this._buf_list.push(chunk);
if(!this._recv_connection_header) {
if(this._buf_list.total >= connection_header.length) {
var b = Buffer.concat(this._buf_list);
this.emit('ConnectionHeader', b.slice(0, connection_header.length));
back = b.slice(connection_header.length);
backBuffer(this, back);
} else {
cb();
return;
}
}
(いろいろ後略)
};
マジックコード長だけ
切り出し、コールバッ
ク内でHTTP/2.0の文
字列チェック
初期接続済フラグ
WritableStream
HTTP/2.0フレームヘッダのパース
(バイナリーデータの解析)
function parseFrameHeader(buf) {
var offset = 0;
var length = buf.readUInt16BE(offset);
offset += 2;
var type = buf.readUInt8(offset);
offset++;
var flags = buf.readUInt8(offset);
offset++;
var stream_id = buf.readUInt32BE(offset) & kUInt31;
return {'length': length, 'type': type, 'flags': flags, 'stream_id': stream_id};
}
バイナリーフレームのデータ解析は Buffer API が必須
ペイロードをパースしたら各フレーム型に応じたコールバックを起動
HTTPバインディング
Nodeコアモジュールと連携
• 結局ほぼ書き直すことに・・・
– 既存のoutgoing/incoming クラスの跡形もなく・・・
• 時間が足りなく dirty hack の嵐 (涙目)
• ServerPush も実装したんだが・・・
• 海外出張直前のバタバタ、一番苦労したとこ
ろ。でも一身上の都合で今回発表は見合わ
せます。(全部作り直してやる)
で、 Hello World! with iij-http2
var http2 = require(‘./lib/http2.js’);
var server = http2.createServer(function(req, res) {
res.writeHead(200, {‘content-type’: ‘text/plain’});
res.end(‘Hello World!’);
});
server.listen(8080);
(SSLサーバでは、証明書とNPN関係のオプション指定を行う)
iij-http2 <-> HTTP/2.0 Chrome
成功!
(TLS+NPNの場合ですが)

Node の HTTP/2.0 モジュール iij-http2 の実装苦労話