C/C++ 素人が http_parser を追ってみた

この記事は Node.js Advent Calendar 2013 - Adventar の15日目です。

Node.js については最新の安定版 v0.10.23 を元にしています。

http_parser とは

Node.js の初代ゲートキーパー Ryan Dahl が開発したC言語で書かれた http parser です。 このライブラリは Node.js の Addons と同様 v8 が C++JavaScript を繋ぎこむ仕組みを利用して読み込まれています。

PythonmeinheldRuBybossan といったサーバでも利用実績があるようです。

Node.js では http クラス内部で利用されています。

http_parser の基本的な使い方

Readme.mdtest.c をご確認ください。

Readme.md に書かれている通りですが

  • http_parser_settings 構造体の変数を用意し、コールバックを登録
  • http_parser 構造体の変数を用意し、HTTP_REQUEST か HTTP_RESPONSE(列挙型)を指定して、http_parser_init 関数で初期化
  • http_parser 構造体と hbttp_parser_settings 構造体の参照、バッファ、バッファの長さを引数として http_parser_execute 関数を実行
  • http_parser_execute 関数は parse した数を戻り値として戻し、http_parser 構造体のメンバに結果を格納、パースの過程で http_parser_settings 構造体に登録されたコールバック関数を利用します。

Node.js 内部での利用方法

前述の通り Addons の仕組みとほぼ同様です。 node_http_parser.cc に実装がありますが

NODE_MODULE(node_http_parser, node::InitHttpParser) で InitHttpParser 関数を node_http_parser という名前に紐付るので、初めて process.binding(’http_parser’) が呼び出された際に InitHttpParser 関数が実行されます。

InitHttpParser 内ではクラス初期化時の処理を C++ のクラス Parser の New と紐付けたり、クラス名 HTTPParser をセットしたり、クラス初期化時に http_parser_type 列挙型を受け取る準備をしたり、execute、finish、reinitialize、pause、resume といったメソッドと C++ で書かれた Parser クラスのメソッドを紐付けたり、コールバック関数の準備をしたりします。

こうした準備を経て、http.js 内から HTTPParser が JavaScriptC++ の間を渡って利用できるようになっています。

初歩的な例

Node.js 公式サイト にある簡単な例を見てみましょう。

var http = require('http');
http.createServer(function (req, res) {
  res.writeHead(200, {'Content-Type': 'text/plain'});
  res.end('Hello World\n');
}).listen(1337, '127.0.0.1');
console.log('Server running at http://127.0.0.1:1337/');

To run the server, put the code into a file `example.js` and 
execute it with the `node` program from the command line:

% node example.js
Server running at http://127.0.0.1:1337/

このスクリプトを実行すると example.js が読み込まれ http クラスが利用されているので InitHttpParser が呼び出されます。 その後サーバが起動し、リクエストを待ち受けはじめます。

このサーバにアクセスすると connection イベントが発生し、connectionListener 内で parsers.alloc() が呼ばれ var parser = new HTTPParser(HTTPParser.REQUEST);が実行されます。 ここで Parser::New が呼び出されます。 Parser::New は引数のキャストとチェックを行った後、http_parser_type を引数に Parser のコンストラクタを呼び出します。 コンストラクタはそのまま引数を Parser::Init に委譲します。

Parser::Init では http_parser_init 関数の呼び出しと各種変数の初期化が実行されます。

こうして初期化された HTTPParser クラスにコールバック関数が設定され、リクエストを受け取る準備が出来ました。

リクエストは SocketOnData で受け取られ、この中で praser.execute(Parser::Execute) が呼び出されます。

http_parser_execute の実行

Parser::Execute は コメント にもありますが、buffer、開始位置、長さを引数として受け取ります。

この後おなじみの C++ への型変換等があっていよいよ http_parser_execute が呼び出されます。

さてどのようにパースが行われているのか見てみると大量の switch-case 文 が並んでいます。

parser->state を元に判定するこの switch-case 文ですが、初期値は http_parser_init 時に決定されています。 http_parser 初期化時の http_parser_type が HTTP_REQUEST なら s_start_req、HTTP_RESPONSE ならば s_start_res です。

こうして状態ごとに parser->state を切り替え、ていねいにパースしていき、パース結果は http_parser 構造体に格納され、最終的に JavaScript へと変換されます。

まとめ

最初に http.js について教えていただいたのは id:jovi0608 さん主催の Node.js 道場でした。 ここで学んだ際に http_parser が C のライブラリであることを認識し、興味を持ったと思います。 また、この時学んだ内部実装が基礎となり http_parser を理解できたと思います。

Node.js 道場は、こちらの本を元に進めていました。 一年前の出版なのでバージョンは進んでしまったとは思いますが基礎的な部分は変わっていないかと思います。 ご興味があったらぜひ読んでみてください。

※ アサマシリンクになっています。よろしければ。。。
電子書籍達人出版会で購入可能です。

Node.js 道場の課題については Qiita にも上がっていますので、こちらもご興味がありましたらどうぞ

http://qiita.com/mazzo46@github/items/1b1fac54d72110ebc508

先日の jovi0608 さんのエントリーにもあったように、最近 Node.js コミュニティでは、非常に精力的にコントリビュートしてきたメンバーが去るという悲しい出来事がありました。
ご当人は http_paser にも精力的にコミットしていたそうです。彼の素晴らしい貢献に対して感謝したいと思います。

以前聞いた話だと http_parser を JavaScript 化するという試みもあるようです。1.0 も間近となり、今後共目が離せないですね。

終わりに

自分は普段 LL 中心で C/C++ はほとんど触ったことがなく、コードを追うのに最初は Eclipse から始めて、途中で gdb を直接使うようになりました。 gdb は慣れてしまうと irb や perl -d などと似たところが多く(多分 gdb を手本にして作られたからですよね)だいぶ手に馴染んできた気がします。 ただ Mac ではうまくブレークポイントが設定出来なかったり、gdb のプロセスを信頼させる おまじない が必要なので、可能ならば Linux 環境を使うとはまりにくいイメージがあります。

この文章には間違いが含まれている可能性があります。ツッコミ歓迎ですので間違いなどありましたら教えていただけると嬉しいです。