代码改变世界

Cowboy 源码分析(七)

2012-05-22 00:18  rhinovirus  阅读(2694)  评论(1编辑  收藏  举报

  上一篇我们讲到了cowboy_requests_sup这个模块,这一篇继续往下走,看下 cowboy_acceptor 这个模块:

%% @private
-module(cowboy_acceptor).

-export([start_link/6]). %% API.
-export([acceptor/7]). %% Internal.

%% API.

-spec start_link(inet:socket(), module(), module(), any(),
    pid(), pid()) -> {ok, pid()}.
start_link(LSocket, Transport, Protocol, Opts,
        ListenerPid, ReqsSup) ->
    Pid = spawn_link(?MODULE, acceptor,
        [LSocket, Transport, Protocol, Opts, 1, ListenerPid, ReqsSup]),
    {ok, Pid}.

%% Internal.

-spec acceptor(inet:socket(), module(), module(), any(),
    non_neg_integer(), pid(), pid()) -> no_return().
acceptor(LSocket, Transport, Protocol, Opts, OptsVsn, ListenerPid, ReqsSup) ->
    Res = case Transport:accept(LSocket, 2000) of
        {ok, CSocket} ->
            {ok, Pid} = supervisor:start_child(ReqsSup,
                [ListenerPid, CSocket, Transport, Protocol, Opts]),
            Transport:controlling_process(CSocket, Pid),
            cowboy_listener:add_connection(ListenerPid,
                default, Pid, OptsVsn);
        {error, timeout} ->
            cowboy_listener:check_upgrades(ListenerPid, OptsVsn);
        {error, _Reason} ->
            %% @todo Probably do something here. If the socket was closed,
            %%       we may want to try and listen again on the port?
            ok
    end,
    case Res of
        ok ->
            ?MODULE:acceptor(LSocket, Transport, Protocol,
                Opts, OptsVsn, ListenerPid, ReqsSup);
        {upgrade, Opts2, OptsVsn2} ->
            ?MODULE:acceptor(LSocket, Transport, Protocol,
                Opts2, OptsVsn2, ListenerPid, ReqsSup)
    end.

  我们先看下 spawn_link/4 这个函数,同样的,列出所有参数的值:

  {ok, LSocket} = cowboy_tcp_transport:listen([{port, 8080}]),

  Transport= cowboy_tcp_transport,

  Protocol = cowboy_http_protocol,

  Opts = [{dispatch, Dispatch}],

  ListenerPid 是cowboy_listener工作进程的进程标识;

  ReqsPid 则是cowboy_requests_sup督程的进程标识;

  Pid = spawn_link(?MODULE, acceptor, [LSocket, Transport, Protocol, Opts, 1, ListenerPid, ReqsSup]),  spawn_link/3

  erlang doc 如下: http://www.erlang.org/doc/man/erlang.html#spawn_link-3 具体我贴过来了,如下:

  spawn_link(Module, Function, Args) -> pid()

    Types:

      Module = Function = atom()
      Args = [term()]

    Returns the pid of a new process started by the application of Module:Function to Args. A link is created between the calling process and the new process, atomically. Otherwise works like spawn/3.

  大意如下:创建一个进程,并执行 M:F(A),并返回进程id。新创建的进程链接到调用进程。

  在这里,M:F(A) = ?MODULE:acceptor(LSocket, Transport, Protocol, Opts, 1, ListenerPid, ReqsSup),

  这里注意下,我们

  那么接下来我们看下 cowboy_acceptor:acceptor/7 方法:

  Res = case Transport:accept(LSocket, 2000) of 

  也就是 Res = case cowboy_tcp_transport:accept(LSocket, 2000) of

  我们看下这个模块的这个函数,通过socket监听接受到来的连接请求:

%% @doc Accept an incoming connection on a listen socket.
%% @see gen_tcp:accept/2
-spec accept(inet:socket(), timeout())
    -> {ok, inet:socket()} | {error, closed | timeout | atom()}.
accept(LSocket, Timeout) ->
    gen_tcp:accept(LSocket, Timeout).

  通过socket监听接受到来的连接请求。ListenSocket必须是由listen/2返回的。Timeout指定了一个毫秒级的超时,默认为infinity。

  如果连接建立则返回{ok,Socket},如果ListenSocket关闭了则返回{error,closed},如果在指定的时间内没有建立连接, 则返回{error,timeout}。如果某些东西出错,也可能返回一个POSIX错误。可能的错误值请参考inet(3)。

  erlang doc :http://www.erlang.org/doc/man/gen_tcp.html

  有朋友翻译成中文版:http://blog.csdn.net/leechyu89/article/details/7305673 这边感谢这位素不相识的朋友。

  接着回到 cowboy_acceptor:acceptor/7 方法

        {ok, CSocket} ->
            {ok, Pid} = supervisor:start_child(ReqsSup,
                [ListenerPid, CSocket, Transport, Protocol, Opts]),
            Transport:controlling_process(CSocket, Pid),
            cowboy_listener:add_connection(ListenerPid,
                default, Pid, OptsVsn);

  在调用  case cowboy_tcp_transport:accept(LSocket, 2000) of 如果返回 {ok, CSocket}, 也就是和 LSocket 连接建立,则启动一个cowboy_requests_sup督程的子进程,代码如下:

   {ok, Pid} = supervisor:start_child(ReqsSup, [ListenerPid, CSocket, Transport, Protocol, Opts]),

  不知道大家还记得吗,上一篇,我们提到,cowboy_requests_sup督程,所有的子进程都是动态添加的同一个进程的实例,这边,我们再贴下代码:  

init([]) ->
    {ok, {{simple_one_for_one, 0, 1}, [{?MODULE, {?MODULE, start_request, []},
        temporary, brutal_kill, worker, [?MODULE]}]}}.

  那么,当我们调用 supervisor:start_child/2 方法时,supervisor 内部会 调用 cowboy_requests_sup:start_request/5 方法。那么我们看下这个方法的代码:

-spec start_request(pid(), inet:socket(), module(), module(), any())
    -> {ok, pid()}.
start_request(ListenerPid, Socket, Transport, Protocol, Opts) ->
    Protocol:start_link(ListenerPid, Socket, Transport, Opts).

  这个方法也是一行代码:

  Protocol:start_link(ListenerPid, Socket, Transport, Opts).

  = cowboy_http_protocol:start_link(ListenerPid, Socket, Transport, Opts).

  我们看下 cowboy_http_protocol:start_link/4 方法,代码如下:

%% @doc Start an HTTP protocol process.
-spec start_link(pid(), inet:socket(), module(), any()) -> {ok, pid()}.
start_link(ListenerPid, Socket, Transport, Opts) ->
    Pid = spawn_link(?MODULE, init, [ListenerPid, Socket, Transport, Opts]),
    {ok, Pid}.

  这边也是启动一个进程,并链接到调用进程。上面已经详细介绍了这个方法。我们跳到 cowboy_http_protocol:init/1 这个方法:

%% @private
-spec init(pid(), inet:socket(), module(), any()) -> ok.
init(ListenerPid, Socket, Transport, Opts) ->
    Dispatch = proplists:get_value(dispatch, Opts, []),
    MaxEmptyLines = proplists:get_value(max_empty_lines, Opts, 5),
    MaxKeepalive = proplists:get_value(max_keepalive, Opts, infinity),
    MaxLineLength = proplists:get_value(max_line_length, Opts, 4096),
    OnRequest = proplists:get_value(onrequest, Opts),
    OnResponse = proplists:get_value(onresponse, Opts),
    Timeout = proplists:get_value(timeout, Opts, 5000),
    URLDecDefault = {fun cowboy_http:urldecode/2, crash},
    URLDec = proplists:get_value(urldecode, Opts, URLDecDefault),
    ok = cowboy:accept_ack(ListenerPid),
    wait_request(#state{listener=ListenerPid, socket=Socket, transport=Transport,
        dispatch=Dispatch, max_empty_lines=MaxEmptyLines,
        max_keepalive=MaxKeepalive, max_line_length=MaxLineLength,
        timeout=Timeout, onrequest=OnRequest, onresponse=OnResponse,
        urldecode=URLDec}).

  前面 7行都是从 Opts 中读取配置,而 Opts = [{dispatch, Dispatch}],除了第一行,其他都用的默认配置,具体每个配置是什么意思,这里先不说,

  从第 8行开始看:

  URLDecDefault = {fun cowboy_http:urldecode/2, crash},

  cowboy_http:urldecode/2 内容如下:

%% @doc Decode a URL encoded binary.
%% The second argument specifies how to handle percent characters that are not
%% followed by two valid hex characters. Use `skip' to ignore such errors,
%% if `crash' is used the function will fail with the reason `badarg'.
-spec urldecode(binary(), crash | skip) -> binary().
urldecode(Bin, OnError) when is_binary(Bin) ->
    urldecode(Bin, <<>>, OnError).

  大概意思就是 对二进制进行URL解码。这个模块先不详细讲了,下一篇,会详细介绍。

  URLDec = proplists:get_value(urldecode, Opts, URLDecDefault), 这一行也是从配置项中获取对应配置,不存在,则使用URLDecDefault

  这行,ok = cowboy:accept_ack(ListenerPid), 我们来简单看下:

%% @doc Acknowledge the accepted connection.
%%
%% Effectively used to make sure the socket control has been given to
%% the protocol process before starting to use it.
-spec accept_ack(pid()) -> ok.
accept_ack(ListenerPid) ->
    receive {shoot, ListenerPid} -> ok end.

  在这里读取消息,{shoot, ListenerPid},如果读取到,就继续往下走。那么时候究竟在什么地方会发送这个消息呢?

  这边模块比较多,可能大家容易晕,建议对着代码看,就好了。

  我们回到 cowboy_acceptor:acceptor/7 方法,我们继续往下看:

  Transport:controlling_process(CSocket, Pid),

  = cowboy_tcp_transport:controlling_process(CSocket, Pid),

  具体代码如下:

%% @doc Assign a new controlling process <em>Pid</em> to <em>Socket</em>.
%% @see gen_tcp:controlling_process/2
-spec controlling_process(inet:socket(), pid())
    -> ok | {error, closed | not_owner | atom()}.
controlling_process(Socket, Pid) ->
    gen_tcp:controlling_process(Socket, Pid).

  这个是意思是:把一个套接字的控制进程改为新的控制进程NewPid,还是老规矩,给出 erlang doc 地址:http://www.erlang.org/doc/man/gen_tcp.html

  controlling_process(Socket, Pid) -> ok | {error, Reason}

    Types:

      Socket = socket()
      Pid = pid()
      Reason = closed | not_owner | inet:posix()

    Assigns a new controlling process Pid to Socket. The controlling process is the process which receives messages from the socket. If called by any other process than the current controlling process, {error, eperm} is returned.

  不知道为什么这里要这么做,先留下疑问吧,之后再去解决。

  我们又要回到 cowboy_acceptor:acceptor/7 方法,看下:

  cowboy_listener:add_connection(ListenerPid, default, Pid, OptsVsn);

  我们看下这个方法的代码:

%% @doc Add a connection to the given pool in the listener.
%%
%% Pools of connections are used to restrict the maximum number of connections
%% depending on their type. By default, Cowboy add all connections to the
%% pool <em>default</em>. It also checks for the maximum number of connections
%% in that pool before accepting again. This function only returns when there
%% is free space in the pool.
%%
%% When a process managing a connection dies, the process is removed from the
%% pool. If the socket has been sent to another process, it is up to the
%% protocol code to inform the listener of the new <em>ConnPid</em> by removing
%% the previous and adding the new one.
%%
%% This function also returns whether the protocol options have been modified.
%% If so, then an {upgrade, ProtoOpts, OptsVsn} will be returned instead of
%% the atom 'ok'. The acceptor can then continue with the new protocol options.
-spec add_connection(pid(), atom(), pid(), non_neg_integer())
    -> ok | {upgrade, any(), non_neg_integer()}.
add_connection(ServerPid, Pool, ConnPid, OptsVsn) ->
    gen_server:call(ServerPid, {add_connection, Pool, ConnPid, OptsVsn},
        infinity).

  这个方法,也是一行代码,ServerPid 是cowboy_listener工作进程的进程标识;所以这里就是给这个工作进程发送一条异步消息,gen_server:call/3 不详细解释了,参看 Erlang OTP 设计原则。

  当这个 cowboy_listener 工作进程收到这条异步消息时,又会做些什么呢,我们下一篇继续分析,谢谢大家。