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

24
Node HTTP/2.0 モジュール iij-http2 の実装苦労話 IIJ 大津 繁樹 201382110回東京Node学園

Upload: shigekiohtsu

Post on 20-Aug-2015

3.180 views

Category:

Documents


5 download

TRANSCRIPT

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

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

IIJ 大津 繁樹

2013年8月21日

第10回東京Node学園

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

自己紹介 • 株式会社インターネットイニシアティブ(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モジュールの開発をしています。

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

ちょっとNodeの最新トピックス(PR5464)

var EventEmitter = require("events") ; var emitter = new EventEmitter(); と書けるようになりました!(ただしv0.11.6~)

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

あと、ストリームについては、3か月前 こうでした。

http://www.slideshare.net/shigeki_ohtsu/stream2-kihon

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

もう時代は先に (v0.11.5~)

• old mode/new mode はなくなり flowing mode/paused mode に、 • stream1 + stream2 = streams3 • data イベント復活

• ただし以前のAPIと互換性はありますのでご安心を

今回のiij-http2は Streams3 を利用

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

さて本題へ、

Nodeを使って新しいプロトコル(HTTP/2.0)を実装したというお話です。

(コードはまだ未公開です。テスト追加やらドキュメント整備やらリファクタやら・・・)

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

SPDY、HTTP/2.0について http://www.iij.ad.jp/company/development/tech/activities/spdy/

これ読んでください。

テキストベースのプロトコルからバイナリープロトコルへ

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

年 月 トピック

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年春に仕様化完了を目指す

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

相互試験の細かい話は、 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/

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

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バイト

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

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種だけに。

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

iij-http2 の基本構成 (HTTPバインディング部分は除く)

Connection (TCP接続)

OnDATA(frame) OnHEADERS(frame) ・・・・ onWINDOW_UPDATE(frame)

Frame

HEADERS DATA WINDOW_UPDATE ・・・・

Stream (多重化)

stream_id priority _state

ストリーム生成

ソケットデータをフレーム解析& コールバック

ストリーム状態遷移を管理

WritableStreamを利用

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

HTTP Server のおさらい(v0.11)

TCP socket サーバに HTTP/HTTPS サーバがぶらさがっているのよ

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

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化は楽

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

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でこれと同じ処理ができるようにしよう

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

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 を実装

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

HTTP/2.0の接続方法(第2段階目)

505249202a20485454502f322e300d0a0d0a534d0d0a0d0a

PRI * HTTP/2.0¥r¥n ¥r¥n SM¥r¥n ¥r¥n

クライアントから謎の24byteのマジックコードをサーバに送り、最終チェックする。

SETTINGS(初期設定値の交換)

HEADERS(HTTPレスポンス)

HEADERS(HTTPリクエスト)

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

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

レスポンスオブジェクト生成

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

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 も同じでした。

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

初期接続処理 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

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

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 が必須 ペイロードをパースしたら各フレーム型に応じたコールバックを起動

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

HTTPバインディング Nodeコアモジュールと連携

• 結局ほぼ書き直すことに・・・

–既存のoutgoing/incoming クラスの跡形もなく・・・

• 時間が足りなく dirty hack の嵐 (涙目)

• ServerPush も実装したんだが・・・

• 海外出張直前のバタバタ、一番苦労したところ。でも一身上の都合で今回発表は見合わせます。(全部作り直してやる)

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

で、 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関係のオプション指定を行う)

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

iij-http2 <-> HTTP/2.0 Chrome

成功!

(TLS+NPNの場合ですが)