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

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

[erlang]

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

[/erlang]

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

[erlang]

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

[/erlang]

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

[erlang]

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

[/erlang]

而在mochiweb_socket_server中,会重新解析loop

[erlang]

parse_options([{loop, Loop} | Rest], State) ->

parse_options(Rest, State#mochiweb_socket_server{loop=Loop});

[/erlang]

此时record中的loop将会是

[erlang]

{mochiweb_http, loop, [HttpLoop]},

[/erlang]

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

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

[erlang]

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.

[/erlang]

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

[erlang]

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.

[/erlang]

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

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

[erlang]

new_request(Socket, Request, RevHeaders) ->

ok = mochiweb_socket:setopts(Socket, [{packet, raw}]),

mochiweb:new_request({Socket, Request, lists:reverse(RevHeaders)}).

[/erlang]

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

[erlang]

new_request({Socket, {Method, {abs_path, Uri}, Version}, Headers}) ->

mochiweb_request:new(Socket,

Method,

Uri,

Version,

mochiweb_headers:make(Headers));

[/erlang]

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

我们来看mochiweb_request的模块声明

[erlang]

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

[/erlang]

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

然后来看call_body方法

[erlang]

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

[/erlang]

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

[erlang]

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

start(Options = [{port, _Port}]) ->

mochiweb_http:start([{name, ?MODULE}, {loop, ?LOOP} | Options]).

[/erlang]

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

而call_body的第一个函数则是带参数版的而已。

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

[erlang]

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.

[/erlang]

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

[erlang]

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

[/erlang]

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

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

[erlang]

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

[/erlang]

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

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