首页 > erlang, 源码阅读 > mochiweb源码分析(二)

mochiweb源码分析(二)

原创文章,转载请注明: 转载自pagefault

本文链接地址: mochiweb源码分析(二)

这次主要来看mochiweb如何处理http协议以及如何将外部模块加载到mochiweb框架中。

首先在上一篇的分析最后,我们知道当accept句柄之后,mochiweb最终会调用call_loop方法,那么我们就从call_loop开始

call_loop({M, F}, Socket) ->
    M:F(Socket);
call_loop({M, F, [A1]}, Socket) ->
    M:F(Socket, A1);
call_loop({M, F, A}, Socket) ->
    erlang:apply(M, F, [Socket | A]);
call_loop(Loop, Socket) ->
    Loop(Socket).


可以看到call_loop一共有重载了4次,其中4个函数不同点只是第一个参数,这里有这么多重载是因为mochiweb并不是简单的只是一个http server,它还可以直接作为一个裸socket server(这个后续再说).而这里mochiweb调用call_loop时,第一个参数就是Loop,而这个Loop是什么呢,我们来从mochiweb启动开始来分析。先来回顾一开始解析option的部分。

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).

可以看到这里最终options1 里面loop会是这样子

{loop, {?MODULE, loop, [HttpLoop]}}

而在mochiweb_socket_server中,会重新解析loop

parse_options([{loop, Loop} | Rest], State) ->
    parse_options(Rest, State#mochiweb_socket_server{loop=Loop});

此时record中的loop将会是

{mochiweb_http, loop, [HttpLoop]},

而这个也就是会最终传递给call_loop。于是经过匹配,最终call_loop会调用mochiweb_http的loop方法,而第一个参数是对应的socket,第二个是[HttpLoop]也就是自定义模块所传递进来的loop。

于是我们来看mochiweb_http的loop方法.

loop(Socket, Body) ->
    ok = mochiweb_socket:setopts(Socket, [{packet, http}]),
    request(Socket, Body).

request(Socket, Body) ->
    ok = mochiweb_socket:setopts(Socket, [{active, once}]),
    receive
        {Protocol, _, {http_request, Method, Path, Version}} when Protocol == http orelse Protocol == ssl ->
            ok = mochiweb_socket:setopts(Socket, [{packet, http}]),
            headers(Socket, {Method, Path, Version}, [], Body, 0);
        {Protocol, _, {http_error, "\r\n"}} when Protocol == http orelse Protocol == ssl ->
            request(Socket, Body);
        {Protocol, _, {http_error, "\n"}} when Protocol == http orelse Protocol == ssl ->
            request(Socket, Body);
        {tcp_closed, _} ->
            mochiweb_socket:close(Socket),
            exit(normal);
        {ssl_closed, _} ->
            mochiweb_socket:close(Socket),
            exit(normal);
        _Other ->
            handle_invalid_request(Socket)
    after ?REQUEST_RECV_TIMEOUT ->
        mochiweb_socket:close(Socket),
        exit(normal)
    end.

首先设置socket属性,由于我们这里是http协议,因此就使用{packet,http},然后调用request方法来处理请求。这里注意在读取之前,设置socket属性为{active, once},也就是半阻塞模式。接收完毕后,会继续调用headers来接收并解析http header.

headers(Socket, Request, Headers, _Body, ?MAX_HEADERS) ->
    %% Too many headers sent, bad request.
    ok = mochiweb_socket:setopts(Socket, [{packet, raw}]),
    handle_invalid_request(Socket, Request, Headers);
headers(Socket, Request, Headers, Body, HeaderCount) ->
    ok = mochiweb_socket:setopts(Socket, [{active, once}]),
    receive
        {Protocol, _, http_eoh} when Protocol == http orelse Protocol == ssl ->
            Req = new_request(Socket, Request, Headers),
            call_body(Body, Req),
            ?MODULE:after_response(Body, Req);
        {Protocol, _, {http_header, _, Name, _, Value}} when Protocol == http orelse Protocol == ssl ->
            headers(Socket, Request, [{Name, Value} | Headers], Body,
                    1 + HeaderCount);
        {tcp_closed, _} ->
            mochiweb_socket:close(Socket),
            exit(normal);
        _Other ->
            handle_invalid_request(Socket, Request, Headers)
    after ?HEADERS_RECV_TIMEOUT ->
        mochiweb_socket:close(Socket),
        exit(normal)
    end.

可以看到如果header超过了规定大小的话,就会报错,如果是正常的头的话,会一直递归解析,直到协议解析完毕(http_eoh),然后调用new_request来创建一个request对象,并调用call_body,最后调用after_response.

接下来就来看这三个函数,首先是new_request

new_request(Socket, Request, RevHeaders) ->
    ok = mochiweb_socket:setopts(Socket, [{packet, raw}]),
    mochiweb:new_request({Socket, Request, lists:reverse(RevHeaders)}).

可以看到由于接下来是可能会接收http body,因此这里就设置位packet raw,然后调用mochiweb的new_request创建一个新的request对象。这里new_request重载了很多次,我们就只看简单的分析一个

new_request({Socket, {Method, {abs_path, Uri}, Version}, Headers}) ->
    mochiweb_request:new(Socket,
                         Method,
                         Uri,
                         Version,
                         mochiweb_headers:make(Headers));

它会直接调用 mochiweb_request的new,这里就有一个很需要注意的地方了,那就是 mochiweb_request使用了Parameterized Modules,这个东西暂时erlang官方的文档还没更新,不过详细可以看峰爷的blog:http://mryufeng.iteye.com/blog/477376 以及这篇文章: http://www.trapexit.org/Parameterized_Modules

我们来看mochiweb_request的模块声明

-module(mochiweb_request, [Socket, Method, RawPath, Version, Headers]).

这样子,当new了之后,我们就能通过它export出来的几个方法来取得对应的值,就有点像java了。

然后来看call_body方法

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).

在headers调用call_body时,传递进的第一个参数,其实就是外部模块传递进来的loop这个tuple。因此我们来看keepalive传递进来的loop到底是什么。

-define(LOOP, {?MODULE, loop}).

start(Options = [{port, _Port}]) ->
    mochiweb_http:start([{name, ?MODULE}, {loop, ?LOOP} | Options]).

可以看到就是一个简单的tuple,{?MODULE, loop},所以此时call_body将会调用第二个函数,直接调用回调模块的loop方法。
而call_body的第一个函数则是带参数版的而已。

然后我们来看after_response方法,这个方法主要是用来判断是否是keepalive连接,然后是否需要关闭当前连接,如果不需要关闭,则再次进入循环。

after_response(Body, Req) ->
    Socket = Req:get(socket),
    case Req:should_close() of
        true ->
            mochiweb_socket:close(Socket),
            exit(normal);
        false ->
            Req:cleanup(),
            erlang:garbage_collect(),
            ?MODULE:loop(Socket, Body)
    end.

这里其他的都很简单,我们主要来看should_close方法。

should_close() ->
    ForceClose = erlang:get(?SAVE_FORCE_CLOSE) =/= undefined,
    DidNotRecv = erlang:get(?SAVE_RECV) =:= undefined,
    ForceClose orelse Version < {1, 0}
        %% Connection: close
        orelse get_header_value("connection") =:= "close"
        %% HTTP 1.0 requires Connection: Keep-Alive
        orelse (Version =:= {1, 0}
                andalso get_header_value("connection") =/= "Keep-Alive")
        %% unread data left on the socket, can't safely continue
        orelse (DidNotRecv
                andalso get_header_value("content-length") =/= undefined
                andalso list_to_integer(get_header_value("content-length")) > 0)
        orelse (DidNotRecv
                andalso get_header_value("transfer-encoding") =:= "chunked").

这个方法主要是判断是否需要关闭连接,不过这里要注意使用了进程字典,也就是我们在外部模块可以设置是否要强制关闭。

我们最后就来看下mochiweb中进程字典所保存的元素。

-define(SAVE_QS, mochiweb_request_qs).
-define(SAVE_PATH, mochiweb_request_path).
-define(SAVE_RECV, mochiweb_request_recv).
-define(SAVE_BODY, mochiweb_request_body).
-define(SAVE_BODY_LENGTH, mochiweb_request_body_length).
-define(SAVE_POST, mochiweb_request_post).
-define(SAVE_COOKIE, mochiweb_request_cookie).
-define(SAVE_FORCE_CLOSE, mochiweb_request_force_close).

而进程字典的优缺点可以看峰爷的这篇blog: http://mryufeng.iteye.com/blog/435642

可以看到mochiweb把一些使用很频繁的都放在了进程字典中,比如url中的参数(SAVE_QS),比如cookie(SAVE_COOKIE),比如body(SAVE_BODY)等。

Share
  1. 本文目前尚无任何评论.
  1. 本文目前尚无任何 trackbacks 和 pingbacks.