代码改变世界

mochiweb 源码阅读(十八)

2012-08-14 00:07  rhinovirus  阅读(2107)  评论(0编辑  收藏  举报

  大家好,这两天简单测试了下 erlang-mysql-driver,erlang-mysql-driver 是 MySQL 的 Erlang 语言驱动程序。代码可通过 SVN 获取:

  svn checkout http://erlang-mysql-driver.googlecode.com/svn/trunk/ erlang-mysql-driver-read-only

  以后再分享给大家,今天继续来看mochiweb源码,依然是看下面几行:

        {Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl ->
            Req = new_request(Socket, Request, Headers),
            call_body(Body, Req),
            ?MODULE:after_response(Body, Req);

  上一篇,我们看完了:Req = new_request(Socket, Request, Headers),这一行,今天继续往下,看下:call_body(Body, Req),这行,这里调用:mochiweb_http:call_body/2函数:

call_body({M, F, A}, Req) ->
    erlang:apply(M, F, [Req | A]);
call_body({M, F}, Req) ->
    M:F(Req);
call_body(Body, Req) ->
    Body(Req).

  这里我们来回忆下Body的定义,结合下上文,回忆下:

  mochiweb_http:parse_options/1函数:

parse_options(Options) ->
    {loop, HttpLoop} = proplists:lookup(loop, Options),
    Loop = {?MODULE, loop, [HttpLoop]},
    Options1 = [{loop, Loop} | proplists:delete(loop, Options)],
    mochilists:set_defaults(?DEFAULTS, Options1).  

  这里HttpLoop就是Body,而这个参数是获取配置文件中loop的值,也就是在mochiweb_example_web:start/1函数定义:

start(Options) ->
    {DocRoot, Options1} = get_option(docroot, Options),
    Loop = fun (Req) ->
                   ?MODULE:loop(Req, DocRoot)
           end,
    Profile_fun = fun (Proplist) -> 
              io:format("Proplist = ~p~n", [Proplist]) 
          end,
    mochiweb_http:start([{name, ?MODULE}, {loop, Loop}, {profile_fun, Profile_fun} | Options1]).

  我们可以看到,这个匿名函数,接受一个参数Req,然后调用?MODULE:loop/2函数,也就是:mochiweb_example_web:loop/2函数,注意:第二个参数的值为:DocRoot,这种用法通常称之为:闭包。大家可以查看《Erlang/OTP并发编程实战》第二章,第2.7.2小节,关于匿名fun函数中闭包的内容。

  不知道大家能不能看出接下来mochiweb_http:call_body/2函数会调用第几个分支了吗?

  答案是,第三个分支,假如我们希望函数调用第二个分支,大家知道怎么去修改吗?

  正确答案如下:

start(Options) ->
    {DocRoot, Options1} = get_option(docroot, Options),
    Loop = fun (Req) ->
                   ?MODULE:loop(Req, DocRoot)
           end,
    Profile_fun = fun (Proplist) -> 
              io:format("Proplist = ~p~n", [Proplist]) 
          end,
    mochiweb_http:start([{name, ?MODULE}, {loop, {?MODULE, Loop}}, {profile_fun, Profile_fun} | Options1]).

  我修改loop配置项值为:{loop, {?MODULE, Loop}},那么它会匹配第二分支:

call_body({M, F}, Req) ->
    M:F(Req);

  当然这里并不能简单的这样修改,因为这种调用函数的方法,会破坏闭包。

  DocRoot这个参数的值,定义如下:

P = mochiweb_example_deps:local_path(["priv", "www"])

  值为:P = "/home/administrator/workplace/mochiweb_example/priv/www",最后我们来看下mochiweb_example_web:loop/2函数实现:

loop(Req, DocRoot) ->
    "/" ++ Path = Req:get(path),
    try
        case Req:get(method) of
            Method when Method =:= 'GET'; Method =:= 'HEAD' ->
                case Path of
                    _ ->
                        Req:serve_file(Path, DocRoot)
                end;
            'POST' ->
                case Path of
                    _ ->
                        Req:not_found()
                end;
            _ ->
                Req:respond({501, [], []})
        end
    catch
        Type:What ->
            Report = ["web request failed",
                      {path, Path},
                      {type, Type}, {what, What},
                      {trace, erlang:get_stacktrace()}],
            error_logger:error_report(Report),
            %% NOTE: mustache templates need \ because they are not awesome.
            Req:respond({500, [{"Content-Type", "text/plain"}],
                         "request failed, sorry\n"})
    end.

  我们来依次看下这个函数的逻辑:

  "/" ++ Path = Req:get(path),这一行,首先Req是调用mochiweb_request:new/5返回的实例,接着用实例去调用调用get/1函数,匹配的分支如下:

get(path) ->
    case erlang:get(?SAVE_PATH) of
        undefined ->
            {Path0, _, _} = mochiweb_util:urlsplit_path(RawPath),
            Path = mochiweb_util:unquote(Path0),
            put(?SAVE_PATH, Path),
            Path;
        Cached ->
            Cached
    end;

  这个函数,首先调用erlang:get/1,同样,查找erlang doc,地址:http://www.erlang.org/doc/man/erlang.html#get-1,如下图:

  很简单,我就不说了,大家如果不明白,还可以参考《Erlang程序设计》第5章,5.1.18小节关于进程字典的用法。

  有个细节,注意下,不知道大家知道为什么erlang:get/1没有用get/1,而put/2却没有加erlang模块名吗?这里我把erlang模块名去了,保存,提示如下信息:  

  

  原因是:erlang:get/1已经自动导入了,而该模块下又定义了get/1函数,所以如果简写就会有问题,而如果我们给put/2函数加上erlang模块名,是没有问题的。

  ?SAVE_PATH定义如下:  

-define(SAVE_PATH, mochiweb_request_path).

  这个函数,首先判断:mochiweb_request_path这个原子是否存在于进程字典中,如果不存在,则处理RawPath相关逻辑后,存入进程字典,并返回该值;如果存在,则返回缓存的值。

  这个RawPath,为该模块的实例保存的值,接下来我们看下这两个函数:

  mochiweb_util:urlsplit_path/1函数:

%% @spec urlsplit_path(Url) -> {Path, Query, Fragment}
%% @doc Return a 3-tuple, does not expand % escapes. Only supports HTTP style
%%      paths.
urlsplit_path(Path) ->
    urlsplit_path(Path, []).

urlsplit_path("", Acc) ->
    {lists:reverse(Acc), "", ""};
urlsplit_path("?" ++ Rest, Acc) ->
    {Query, Fragment} = urlsplit_query(Rest),
    {lists:reverse(Acc), Query, Fragment};
urlsplit_path("#" ++ Rest, Acc) ->
    {lists:reverse(Acc), "", Rest};
urlsplit_path([C | Rest], Acc) ->
    urlsplit_path(Rest, [C | Acc]).

  测试用例代码如下:

urlsplit_path_test() ->
    {"/foo/bar", "", ""} = urlsplit_path("/foo/bar"),
    {"/foo", "baz", ""} = urlsplit_path("/foo?baz"),
    {"/foo", "", "bar?baz"} = urlsplit_path("/foo#bar?baz"),
    {"/foo", "", "bar?baz#wibble"} = urlsplit_path("/foo#bar?baz#wibble"),
    {"/foo", "bar", "baz"} = urlsplit_path("/foo?bar#baz"),
    {"/foo", "bar?baz", "baz"} = urlsplit_path("/foo?bar?baz#baz"),
    ok.

  mochiweb_util:unquote/1函数:

%% @spec unquote(string() | binary()) -> string()
%% @doc Unquote a URL encoded string.
unquote(Binary) when is_binary(Binary) ->
    unquote(binary_to_list(Binary));
unquote(String) ->
    qs_revdecode(lists:reverse(String)).

  测试用例代码如下:

unquote_test() ->
    ?assertEqual("foo bar",
                 unquote("foo+bar")),
    ?assertEqual("foo bar",
                 unquote("foo%20bar")),
    ?assertEqual("foo\r\n",
                 unquote("foo%0D%0A")),
    ?assertEqual("foo\r\n",
                 unquote(<<"foo%0D%0A">>)),
    ok.

  关于这两个函数,我不打算详细解释,大家看下测试用例就明白了。

  好了,这一篇就到这里,我们下一篇再见,谢谢。