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

4,219 views

Published on

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

  1. 1. Node の HTTP/2.0 モジュール iij-http2 の実装苦労話 IIJ 大津 繁樹 2013年8月21日 第10回東京Node学園
  2. 2. 自己紹介 • 株式会社インターネットイニシアティブ(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モ ジュールの開発をしています。
  3. 3. ちょっとNodeの最新トピックス(PR5464) var EventEmitter = require("events") ; var emitter = new EventEmitter(); と書けるようになりました!(ただしv0.11.6~)
  4. 4. あと、ストリームについては、3か月前 こうでした。 http://www.slideshare.net/shigeki_ohtsu/stream2-kihon
  5. 5. もう時代は先に (v0.11.5~) • old mode/new mode はなくなり flowing mode/paused mode に、 • stream1 + stream2 = streams3 • data イベント復活 • ただし以前のAPIと互換性はありますのでご安心を 今回のiij-http2は Streams3 を利用
  6. 6. さて本題へ、 Nodeを使って新しいプロトコル(HTTP/2.0)を実 装したというお話です。 (コードはまだ未公開です。テスト追加やらドキュメント整備やらリファクタやら・・・)
  7. 7. SPDY、HTTP/2.0について http://www.iij.ad.jp/company/development/tech/activities/spdy/ これ読んでください。 テキストベースのプロトコルからバイナリープロトコルへ
  8. 8. 年 月 トピック 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年春に仕様化完了を目指す
  9. 9. 相互試験の細かい話は、 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/
  10. 10. 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バイト
  11. 11. 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種だけに。
  12. 12. iij-http2 の基本構成 (HTTPバインディング部分は除く) Connection (TCP接続) OnDATA(frame) OnHEADERS(frame) ・・・・ onWINDOW_UPDATE(frame) Frame HEADERSDATA WINDOW_UPDATE・・・・ Stream (多重化) stream_id priority _state ストリー ム生成 ソケットデータを フレーム解析& コールバック ストリーム状態 遷移を管理 WritableStreamを 利用
  13. 13. HTTP Server のおさらい(v0.11) TCP socket サーバに HTTP/HTTPS サーバが ぶらさがっているのよ
  14. 14. 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化は楽
  15. 15. 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でこれと同じ処理ができるようにしよう
  16. 16. 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 を実装
  17. 17. HTTP/2.0の接続方法(第2段階目) 505249202a20485454502f322e300d0a0d0a534d0d0a0d0a PRI * HTTP/2.0¥r¥n ¥r¥n SM¥r¥n ¥r¥n クライアントから謎の24byteのマジックコードをサー バに送り、最終チェックする。 SETTINGS(初期設定値の交換) HEADERS(HTTPレスポンス) HEADERS(HTTPリクエスト)
  18. 18. 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 レスポンスオブジェクト生成
  19. 19. 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 も同じでした。
  20. 20. 初期接続処理 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
  21. 21. 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 が必須 ペイロードをパースしたら各フレーム型に応じたコールバックを起動
  22. 22. HTTPバインディング Nodeコアモジュールと連携 • 結局ほぼ書き直すことに・・・ – 既存のoutgoing/incoming クラスの跡形もなく・・・ • 時間が足りなく dirty hack の嵐 (涙目) • ServerPush も実装したんだが・・・ • 海外出張直前のバタバタ、一番苦労したとこ ろ。でも一身上の都合で今回発表は見合わ せます。(全部作り直してやる)
  23. 23. で、 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関係のオプション指定を行う)
  24. 24. iij-http2 <-> HTTP/2.0 Chrome 成功! (TLS+NPNの場合ですが)

×