mochiweb源码分析(二)
这次主要来看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)等。