コンピュータやソフトウェアのあれこれ@道民(&元道民)
node
[JS][node]Connectのソースを読む(2)
12月 25th
前回の続きで、Connect 1.8.4のソースを読む。今日はミドルウェアから適当に抜粋。残ってるのはミドルウェアだけなので、今日で終わりのつもり(気が向いたら残りも書くかも?)。
middleware/logger.js
アクセスログを書き込むだけだが、connectのミドルウェアの仕組みではリクエスト完了後に割り込むことができない*1のでhackが必要となる。
具体的に言うと、res.endをラップして終了処理に割り込む。そして、何もせずnextを呼べば、リクエストの完了時に割り込んで処理をすることができる。ただし、immediate オプションを渡した場合はこのhackを行わず、ミドルウェアに処理が移った段階でログを吐く*2。
ログに使える:date、:method などは全てloggerオブジェクトのプロパティにメソッドとして定義されており、req, res, field の3引数を渡すことで値を取り出せる。ログのフォーマットとして文字列を渡すと、これらのメソッドを適宜呼び出すような関数へコンパイルされる。
middleware/errorHandler.js
4引数を持つ唯一のミドルウェア。4引数のミドルウェアはエラーハンドリングのために呼び出される。
~accept.indexOf の形式が多用されているが、ビット反転すると-1が0になることを使っていて、文字列が見つからなかった場合にのみfalseとなる。
内部はAcceptヘッダを見てレスポンスをhtml、json、text に分けてスタックを出力するだけの簡単な処理。
utils.pause, resume
ミドルウェアから使われているutils.js 内の関数。任意のObjectについてpauseすると、以後のdataイベントとonイベントがキャッシュされる。キャッシュされたイベントはresumeで再び元のObjectへ渡される。
ミドルウェアから非同期で後続のミドルウェアへ処理を渡そうとする場合は、このメソッドを使う必要がある。そうしないと、後続のミドルウェアがreqからデータを読むためにdataイベントやendイベントを拾おうとしている場合に、それらのミドルウェアがリスナーをしかける前に発生したdataやendのイベントが全て捨てられてしまう。
middleware/basicAuth.js
BASIC認証の実装。Authorization ヘッダがあって認証が成功していればreq.remoteUserへユーザ名をセット、そうでなければ401ステータスを返して認証を促す、というのを愚直に実装している。
basicAuth()の引数の取り方は3パターンで、「user, pass, realm」「cb(user, pass), realm」「cb(user, pass, cb(err, user)), realm」の3通り。最後のパターンは非同期で認証を行うパターンで、認証が完了したらcbへエラー、またはユーザ名を返せばよい。
middleware/router.js
Sinatraライクのルータの実現。ミドルウェアの中でソースは一番行数が多い。
router関数の中で、まずDSLを実現するためにgetやらpostやらのメソッドを持つmethodsというオブジェクトを作っている。利用者はこいつのgetやらpostやらへルーチング情報を渡せばいいというスンポー。getやpostには、Sinatraなどと同じようにPATHとハンドラを渡せるが、ハンドラに加えてミドルウェアも任意個指定ができる。
後、ドキュメント化されてないapp.paramって関数が生えていて、こいつを呼ぶとparamsという変数へコールバックを貯められるが、この辺の実装はかなり怪しい。要はルーチングでマッチしたパラメータに前処理するためのコールバックなのだけど、普通の関数の他にcb(req, res, next, val) というミドルウェアっぽい(けど違う)引数をとるコールバックも指定できて、整理されてない感がある(非同期対応が必要なのはDBから値を読むことを想定してるんだろうけど)。
ルーチングの情報はroutesオブジェクトへメソッドごとに分けて保存される。getやらpostを呼んだときの処理を作っているのがgenerateMethodFunction で、ここでは基本的にroutesの中へルーチング情報を詰め込むだけ。PATHの正規表現へのコンパイルはnormalizePath関数が行う。
本丸は内部のrouter(req, res, next)関数でこいつが実際にルーチングをする。match関数によってマッチするルートがあるか調べられ、マッチした場合はキャプチャーした値がfn.params へ保存される*3。
さらに内部にあるparamという関数は、前述のparamを前処理するコールバックを反復的に適用するために再帰呼び出しされる関数。これはcb(req, res, next, val)のシグネチャを持つ前処理関数が非同期呼び出しに対応しているために必要となる。なお、この前処理の最中でnextへエラーとして'route'という文字列を渡すと、このハンドラでの処理を諦めて大域脱出し、後続のルーターのマッチ処理へ戻る。
paramの再帰の最後で、ハンドラと共に指定したミドルウェアを適用するため、さらに内部にあるnextMiddleware という関数による再帰が起こる。ここでもミドルウェアがnextへエラーとして'route'を渡すと大域脱出できる。ここのミドルウェアの適用は自前で処理しているが、app(req, res, nextMiddleware) の形式のものしかサポートしていないため、エラー処理をするfn(err, req, res, nextMiddleware)の形式のミドルウェアを渡しても機能しない。
ここまでを経て、ようやく最終的にユーザがgetやpostへ指定したハンドラがキックされる。ハンドラ内からnextを呼ぶと、次のルーチングの検索へ移る。その必要がない場合はres.endでレスポンスを完了すればよい。
最終的にマッチするルートがなかった場合は、OPTIONSメソッドの場合はAllow:ヘッダ入りのレスポンスをするか、それにも該当しなければ後続のミドルウェアへ処理を渡す。
作成されたrouterミドルウェアには、.remove, .lookup, .match のメソッドが生えており、作成後にルーティングを削除したり、どんなリクエストがマッチするのかテストしたりできる。
[JS][node]Connectのソースを読む(1)
12月 23rd
node.js 使ったことがなかったので読んでみる。今日はコアの部分。
下準備
githubからcloneしてきて、checkout 1.8.4した。masterじゃないので注意*1。
npm install を実行すると依存モジュールがnode_modules/ 以下に入る。そうするとmake test でテストを走らせられるようになる。expressoというのでテストをしてるっぽいが、OS X上だとテストが通らない。2.x 系のコードだとテストは通るっぽい*2のでまあ気にしない。
依存とかは package.json を見れば分かる。
patch.js
node.js の機能へのモンキーパッチが入っている。具体的にはプライベートフィールド_headerSentを露出させるアクセサ、Set-CookieとContent-Typeヘッダ周りの拡張、HTTPヘッダ吐き出し直前に新しいフックの追加など。
util.js
ユーティリティ。特筆すべき点はないと思う。
connect.js
エントリポイント。createServer関数を提供しているが、こいつは単にhttp.Serverやhttps.Serverのコンストラクタを呼んでるだけ。typeof で引数を判定して関数のオーバーロードを実現するのはJSの常套句。
また、middleware の一覧の作成もここで行っている。middleware/直下で拡張子が.jsのファイルを片っ端から記憶する。readdirSync でロードしてるのでブロックするが、起動時なので実害はないはず。ここでは各middleware へのアクセサを作るだけで、ロードは利用時まで遅延して行われる。
http.js
ミドルウェアの機能を実現している心臓部。node.jsのhttp.Serverを継承している。(ドキュメントにはない気がするけど)http.Serverのコンストラクタにはrequestイベントのリスナを渡すことになっており、handleメソッドをリスナとして登録している。
handleの前にまずuseメソッド。ミドルウェア(後述)の他に、http.ServerやServer(handlerメソッドを持つもの)のインスタンスを渡せるよう考慮されている。ミドルウェアは登録順にstackプロパティへ保管される。
で、本丸のhandleメソッド。こいつがリスナとなっており、リクエストが発生するごとに適切なミドルウェアを適用してレスポンスを返す責務を担っている。ただし、通常のリスナはreq, resの2引数だが、このメソッドはoutを含んだ3引数となる。これは、handleメソッド自体もミドルウェアとなっているため。
handleメソッド内の記述を読むと、middleware の仕様が読み取れる。具体的には、err, req, res, nextの4引数をとる関数か、req, res, nextの3(以下)引数をとる関数となる。前者はエラーハンドリング、後者は通常の処理を担当している*3。
middlewareのnextという引数がコールバックとなっていて、この引数を呼び出すと次のミドルウェアへ処理が移る。後続のmiddlewareへ処理を渡さない場合は、nextを呼ばずにres.end を呼んでレスポンスを完了するとよい。
nextはエラーの運搬も担当している。全てのミドルウェアを処理してもレスポンスが返せなかった場合は、next経由でエラーが報告されていれば500、そうでなければ404ステータスとしてレスポンスを返している。
https.js
node.js の https.Server を継承したServerを作る。前述のhttp.js のServerからメソッドをコピーしているので、実質はhttp.jsと同じ。