node の http/2.0 モジュール iij-http2 の実装苦労話
Post on 20-Aug-2015
3.180 Views
Preview:
TRANSCRIPT
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
HEADERS DATA 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の場合ですが)
top related