2011年11月16日水曜日

MochiWebで学ぶErlang(mochiweb_http)

はじめに



MochiWebはErlangで書かれた軽量HTTPサーバーです。
Erlang使いを志すならMochiWebのコードを読むと良いらしいので読んでみます。



MochiWebはMITライセンスのようです。



HTTPサーバーの起動はmochiweb_httpモジュールのstart関数により行います。
なので、mochiweb_httpから順に流れを追ってみます。



mochiweb_http.erlは単体テストを入れて250行程度。




ファイルの先頭部分



参考






%% @author Bob Ippolito <bob@mochimedia.com>
%% @copyright 2007 Mochi Media, Inc.

%% @doc HTTP server.


Erlangの一行コメントは「%」です。



「@xxx」はErlang/OTP付属のドキュメントジェネレータであるEDocのタグです。




-module(mochiweb_http).
-author('bob@mochimedia.com').
-export([start/1, start_link/1, stop/0, stop/1]).
-export([loop/2]).
-export([after_response/2, reentry/1]).
-export([parse_range_request/1, range_skip_length/2]).

-define(REQUEST_RECV_TIMEOUT, 300000). %% timeout waiting for request line
-define(HEADERS_RECV_TIMEOUT, 30000). %% timeout waiting for headers

-define(MAX_HEADERS, 1000).
-define(DEFAULTS, [{name, ?MODULE},
{port, 8888}]).


シングルクオートで囲まれた値は文字列ではなくアトムです。



「-」から始まるものにはModule Attribute、Preprocessor、レコード定義、
定義済みマクロ(?FILEおよび?LINE)の変更、型や関数の仕様記述があります。



Module Attributeはユーザが自由に定義でき、以下の関数でそのモジュールに定義された
値の一覧を取得できます。




Module:module_info(attributes)


定義済みのModule Attributeその他は以下のとおり。




  • module : モジュール名定義

  • export : 関数のエクスポート

  • import : 関数のインポート

  • compile : コンパイラオプション

  • vsn : モジュールのバージョン

  • behaviour : ビヘイビアのコールバックモジュールであると宣言

  • record : レコード定義

  • include : ファイルインクルード。主にヘッダに対して使う

  • include_lib : includeとほぼ同じ。探索パスが異なる

  • define : マクロ定義

  • undef : マクロを未定義状態にする

  • ifdef : マクロが定義されている場合のマクロ制御フロー

  • ifndef : マクロが未定義である場合のマクロ制御フロー

  • else : ifdef/ifndefとあわせて利用する。ifdef/ifndefでない場合

  • endif : ifdef/ifndef/elseの終端

  • file : 定義済みマクロ?FILEおよび?LINEの値を変更する

  • spec : 関数の仕様。EDocなど以外に影響はない(たぶん)

  • type : 型の仕様。EDocなど以外に影響はない(たぶん)



マクロの定義と使用法は以下のとおり。




%% 定義
-define(Const, Replacement).
%%% 引数を文字列に展開するには「??Arg」を使う
-define(TEST(Exp), io:format("~s : ~p~n", [??Exp, Exp])).
%% 使用
?Const.
?TEST(1 + 2).
%% => "1 + 2 : 3"と表示される


定義済みマクロは以下。




  • ?MODULE : モジュール名(アトム)

  • ?MODULE_STRING : モジュール名(文字列)

  • ?FILE : ファイル名

  • ?LINE : 行番号

  • ?MACHINE : 'BEAM'



include対象のパスに環境変数を利用することができます。




%% $PROJECT_ROOTはos:getenv("PROJ_ROOT")の戻り値に展開される
-include("$PROJ_ROOT/path/lib.hrl").



mochiweb_httpの関数



関数の処理や初めて知ったor気になった点など。




  • parse_options


    • start関数に渡されたオプションをパースして返す

    • オプションは属性リスト(proplists)

    • mochilists:set_defaults関数で未定義オプションにデフォルト値を設定(ポート番号)

    • 戻り値のloopオプション = {?MODULE, loop, [もともと指定されていたloop用関数]}


  • stop


    • mochiweb_socket_server:stopを呼び出す


  • start


    • mochiweb_socket_server:startを呼び出す

    • サーバーの処理はmochiweb_socket_serverに記述されている


  • loop


    • mochiweb_socket:setoptsでソケットに{packet, http}をセットしてrequestを呼ぶ

    • {packet, http}とするとパケット受信時にHTTPヘッダをパースしたものを取得できる

    • mochiweb_socketモジュールはSSL対応か否かによりgen_tcpとsslを使い分けるラッパー


  • request


    • HTTPリクエストの先頭行を読み込む

    • ソケットのオプション{active, once}を指定すると1度だけパケットをメッセージとして受信する


  • headers


    • HTTPリクエストのヘッダを受信する

    • {packet, httph}としているとヘッダの終端まで読み込んだ場合、http_eohを受信する


  • call_body


    • 引数として渡された関数を呼び出す


  • handle_invalid_request


    • 400 Bad Requestを返す

    • Req:respond(...)は、parameterized moduleを利用した表記


  • new_request


    • リクエスト(parameterized module)を作成する


  • after_response


    • mochiweb_requestのshould_closeを呼び出し、ソケットをクローズするかどうかを決定する

    • HTTPのバージョンやKeep-Aliveによって判別するっぽい

    • erlang:garbage_collect()によりガーベージコレクションを明示的に実行できる


  • parse_range_request


    • Rangeヘッダの値をパースする

    • 文字列はリストなので ++ で連結できる

    • 文字列を区切ってリストにするにはstring:tokensを使う

    • 文字列->整数の変換はBIFのlist_to_integerで行える


  • range_skip_length


    • 部分レスポンスの範囲を返す

    • Sizeは対象とするレスポンス(ファイル)のサイズだと思う





Misc



モジュール定義時に、以下のように宣言するとParameterized Moduleになるらしいです。




-module(module_name, [Vars]).


オブジェクト指向チックな書き方ができるようになるようですが、当然変数への代入は1度きりのまま。
mochiwebではプロセス辞書(erlang:put,erlang:getなど)を利用してリクエストの状態を保存しているようです。



mochiweb_requestを読む際にもう少し調べます。




テスト



ifdefにより、TESTマクロが定義されている場合のみ、EUnitを利用したテスト関数が
定義されます。



EUnitでは関数名末尾が「_test」で終わるものは試験ケースとして扱われるようです。



eunit.hrlをインクルードすると、アサーション用マクロが読み込まれます。





TODO




  • EDocについて調べる

  • EUnitについて調べる

  • mochiweb_requestおよびmochiweb_responseを読む

  • mochiweb_socket_serverを読む




2011年11月10日木曜日

Erlang+TCP

ErlangでTCPサーバを書いてみます。
ローカルプロキシを作ってみたいです。




-module(serv).
-compile(export_all).
-compile([start/1]).

-define(TCP_OPTIONS, [binary, {packet, raw}, {active, false}, {reuseaddr, true}]).

start (Port) ->
spawn(?MODULE, init, [Port, self()]).

init(Port, Pid) ->
{ok, Listen} = gen_tcp:listen(Port, ?TCP_OPTIONS),
accept_loop(Listen, Pid).

accept_loop(Listen, Pid) ->
case gen_tcp:accept(Listen) of
{ok, Accept} ->
spawn(?MODULE, accept_handler, [Accept, Pid]),
accept_loop(Listen, Pid)
end.

accept_handler(Accept, Pid) ->
inet:setopts(Accept, [{packet, http}]),
{ok, {http_request, Method, URL, {Major, Minor}}} = gen_tcp:recv(Accept, 0),
Headers = recv_request_headers(Accept),
inet:setopts(Accept, [{packet, raw}]),
Body =
case lists:keyfind('Content-Length', 1, Headers) of
{'Content-Length', Len} when is_integer(Len) ->
gen_tcp:recv(Accept, Len);
_ -> <<>>
end,
io:format("~p, ~p, ~p/~p~n~p~n~p~n",
[Method, URL, Major, Minor, Headers, Body]),
case URL of
{abs_path, Path} ->
Txt = "<html><body><a>not found</a></body></html>",
gen_tcp:send(Accept, "HTTP/1.1 404 not-found\r\nContent-Length: "),
gen_tcp:send(Accept, integer_to_list(length(Txt))),
gen_tcp:send(Accept, "\r\n\r\n"),
gen_tcp:send(Accept, Txt);
{absoluteUrl, Protocol, Host, Port, Path} ->
Txt = "<html><body><a>not found</a></body></html>",
gen_tcp:send(Accept, "HTTP/1.1 404 not-found\r\nContent-Length: "),
gen_tcp:send(Accept, integer_to_list(length(Txt))),
gen_tcp:send(Accept, "\r\n\r\n"),
gen_tcp:send(Accept, Txt)
end,
case lists:keyfind('Connection', 1, Headers) of
{'Connection', "keep-alive"} ->
accept_handler(Accept, Pid);
_ ->
gen_tcp:close(Accept)
end.


recv_request_headers(Accept) ->
recv_request_headers(Accept, []).

recv_request_headers(Accept, Hs) ->
case gen_tcp:recv(Accept, 0) of
{ok, http_eoh} -> % end of header
Hs;
{ok, {http_header, _, Key, _, Val}} ->
recv_request_headers(Accept, [{Key,Val} | Hs])
end.