代码改变世界

Cowboy 源码分析(十九)

2012-06-20 01:18  rhinovirus  阅读(1395)  评论(0编辑  收藏  举报

  这一篇,接着上一篇没有讲完的内容,继续来看cowboy_http_req:reply/4 函数,我们从下面这段代码开始:  

    RespConn = response_connection(Headers, Connection),
    ContentLen = case Body of {CL, _} -> CL; _ -> iolist_size(Body) end,
    HTTP11Headers = case Version of
        {1, 1} -> [{<<"Connection">>, atom_to_connection(Connection)}];
        _ -> []
    end,
    {ReplyType, Req2} = response(Status, Headers, RespHeaders,  [
        {<<"Content-Length">>, integer_to_list(ContentLen)},
        {<<"Date">>, cowboy_clock:rfc1123()},
        {<<"Server">>, <<"Cowboy">>}
    |HTTP11Headers], Req),

  这几行,作用就是构建HTTP响应头,如下图:

  

  我们看下最后几行,调用了cowboy_http_req:response/5函数:

-spec response(cowboy_http:status(), cowboy_http:headers(),
    cowboy_http:headers(), cowboy_http:headers(), #http_req{})
    -> {normal | hook, #http_req{}}.
response(Status, Headers, RespHeaders, DefaultHeaders, Req=#http_req{
        socket=Socket, transport=Transport, version=Version,
        pid=ReqPid, onresponse=OnResponse}) ->
    FullHeaders = response_merge_headers(Headers, RespHeaders, DefaultHeaders),
    Req2 = case OnResponse of
        undefined -> Req;
        OnResponse -> OnResponse(Status, FullHeaders,
            %% Don't call 'onresponse' from the hook itself.
            Req#http_req{resp_headers=[], resp_body= <<>>,
                onresponse=undefined})
    end,
    ReplyType = case Req2#http_req.resp_state of
        waiting ->
            HTTPVer = cowboy_http:version_to_binary(Version),
            StatusLine = << HTTPVer/binary, " ",
                (status(Status))/binary, "\r\n" >>,
            HeaderLines = [[Key, <<": ">>, Value, <<"\r\n">>]
                || {Key, Value} <- FullHeaders],
            Transport:send(Socket, [StatusLine, HeaderLines, <<"\r\n">>]),
            ReqPid ! {?MODULE, resp_sent},
            normal;
        _ ->
            hook
    end,
    {ReplyType, Req2}.

  首先,FullHeaders = response_merge_headers(Headers, RespHeaders, DefaultHeaders),这里我们可以看下,这几个参数的值:

  < Headers = []
  < RespHeaders = []
  < DefaultHeaders = [{<<"Content-Length">>,"12"},
                    {<<"Date">>,<<"Tue, 19 Jun 2012 15:28:24 GMT">>},
                    {<<"Server">>,<<"Cowboy">>},
                    {<<"Connection">>,<<"keep-alive">>}]

  继续,来看下cowboy_http_req:response_merge_headers/3函数,定义如下:

-spec response_merge_headers(cowboy_http:headers(), cowboy_http:headers(),
    cowboy_http:headers()) -> cowboy_http:headers().
response_merge_headers(Headers, RespHeaders, DefaultHeaders) ->
    Headers2 = [{header_to_binary(Key), Value} || {Key, Value} <- Headers],
    merge_headers(
        merge_headers(Headers2, RespHeaders),
        DefaultHeaders).

  这里有个函数,cowboy_http_req:header_to_binary/1代码如下,这里我只列出了部分,由于代码量比较大,而且又很简单:

-spec header_to_binary(cowboy_http:header()) -> binary().
header_to_binary('Cache-Control') -> <<"Cache-Control">>;
header_to_binary('Connection') -> <<"Connection">>;
header_to_binary('Date') -> <<"Date">>;
...

  这个函数,把对应的header字符串转为二进制的格式,知道这个函数的作用,就可以继续看cowboy_http_req:response_merge_headers/3函数:

  Headers2 = [{header_to_binary(Key), Value} || {Key, Value} <- Headers], 这一行,把Headers中的Key,转为二进制格式,组成新的头部,很简单,但是由于Headers = [],那么其实这里最后还是[]。

  merge_headers( merge_headers(Headers2, RespHeaders), DefaultHeaders). 这一行,调用了两次 cowboy_http_req:merge_headers/2函数,函数定义如下:

-spec merge_headers(cowboy_http:headers(), cowboy_http:headers())
    -> cowboy_http:headers().
merge_headers(Headers, []) ->
    Headers;
merge_headers(Headers, [{Name, Value}|Tail]) ->
    Headers2 = case lists:keymember(Name, 1, Headers) of
        true -> Headers;
        false -> Headers ++ [{Name, Value}]
    end,
    merge_headers(Headers2, Tail).

  第一次调用这个函数,传递参数,Headers2, RespHeaders 我们从上面知道,这2个参数均为[],那么第一次调用,则调用第一个分支,返回 Headers = Headers2 = []

  第二次调用传递参数,merge_headers(Headers2, RespHeaders) 返回的[],作为第一个参数,DefaultHeaders则为第二个参数,这里我把几个参数的值列出来,方便理解:

  < Name = <<"Content-Length">>
  < Headers = []
  < Value = "12"
  < Tail = [{<<"Date">>,<<"Tue, 19 Jun 2012 15:53:16 GMT">>},
          {<<"Server">>,<<"Cowboy">>},
          {<<"Connection">>,<<"keep-alive">>}]

  lists:keymember/3 这个函数,我们看下 erlang doc:http://www.erlang.org/doc/man/lists.html#keymember-3

  这里我简单做了几个测试,大家一看就明白这个函数的意思了。

  
  最后,我们能得出如下结果:

  < Headers2 = [{<<"Content-Length">>,"12"}]

  最后,这个函数,是个尾递归函数:merge_headers(Headers2, Tail).

  当Tail = []时,函数回到第一个分支:

  < Headers = [{<<"Content-Length">>,"12"},
             {<<"Date">>,<<"Tue, 19 Jun 2012 16:22:57 GMT">>},
             {<<"Server">>,<<"Cowboy">>},
             {<<"Connection">>,<<"keep-alive">>}]

   好了,这个cowboy_http_req:response_merge_headers/3函数我们可以告一段落了,接下来回到cowboy_http_req:response/5函数的第二段代码:

    Req2 = case OnResponse of
        undefined -> Req;
        OnResponse -> OnResponse(Status, FullHeaders,
            %% Don't call 'onresponse' from the hook itself.
            Req#http_req{resp_headers=[], resp_body= <<>>,
                onresponse=undefined})
    end,

  这里,我们从上下文知道: onresponse=OnResponse=undefined,所以这里返回 Req2 = Req,而另一个分支,等以后具体用到的时候,我再分析,这里就不讲了,下面是Req的值:

  < Req = {http_req,#Port<0.3005>,cowboy_tcp_transport,keepalive,<0.527.0>,
                  'GET',
                  {1,1},
                  undefined,
                  [<<"localhost">>],
                  undefined,<<"localhost">>,8080,[],undefined,<<"/">>,
                  undefined,<<>>,[],
                  [{'Connection',<<"keep-alive">>},
                   {'Accept-Encoding',<<"gzip, deflate">>},
                   {'Accept-Language',<<"zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3">>},
                   {'Accept',<<"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8">>},
                   {'User-Agent',<<"Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:13.0) Gecko/20100101 Firefox/13.0">>},
                   {'Host',<<"localhost">>}],
                  [{'Connection',[<<"keep-alive">>]}],
                  undefined,[],waiting,<<>>,waiting,[],<<>>,undefined,
                  {#Fun<cowboy_http.urldecode.2>,crash}}
  继续看cowboy_http_req:response/5函数的第三段代码:

    ReplyType = case Req2#http_req.resp_state of
        waiting ->
            HTTPVer = cowboy_http:version_to_binary(Version),
            StatusLine = << HTTPVer/binary, " ",
                (status(Status))/binary, "\r\n" >>,
            HeaderLines = [[Key, <<": ">>, Value, <<"\r\n">>]
                || {Key, Value} <- FullHeaders],
            Transport:send(Socket, [StatusLine, HeaderLines, <<"\r\n">>]),
            ReqPid ! {?MODULE, resp_sent},
            normal;
        _ ->
            hook
    end,

  这里,我们使用Debugger可以知道,ReplyType = waiting,那么继续往下看:

  HTTPVer = cowboy_http:version_to_binary(Version), 这里根据版本,返回对应二进制格式,具体代码如下:

%% @doc Convert an HTTP version tuple to its binary form.
-spec version_to_binary(version()) -> binary().
version_to_binary({1, 1}) -> <<"HTTP/1.1">>;
version_to_binary({1, 0}) -> <<"HTTP/1.0">>.

  StatusLine = << HTTPVer/binary, " ", (status(Status))/binary, "\r\n" >>,

  这里生成状态行,如下图:

  

  cowboy_http_req:status/1函数如下,这个函数也比较大,我省略了大部分:

-spec status(cowboy_http:status()) -> binary().
status(100) -> <<"100 Continue">>;
status(101) -> <<"101 Switching Protocols">>;
status(102) -> <<"102 Processing">>;
status(200) -> <<"200 OK">>;
...

  HeaderLines = [[Key, <<": ">>, Value, <<"\r\n">>] || {Key, Value} <- FullHeaders], 这个跟上面差不多意思,如下图:

  

  Transport:send(Socket, [StatusLine, HeaderLines, <<"\r\n">>]),

 =cowboy_tcp_transport:send(Socket, [StatusLine, HeaderLines, <<"\r\n">>]), 这个函数代码如下:

%% @doc Send a packet on a socket.
%% @see gen_tcp:send/2
-spec send(inet:socket(), iolist()) -> ok | {error, atom()}.
send(Socket, Packet) ->
    gen_tcp:send(Socket, Packet).

  代码如此简单,以至于我都不用解释了,这里给连接到服务器的连接Socket发送一个数据包。

  ReqPid ! {?MODULE, resp_sent},

  还记得下面这个函数吗,我们在Cowboy 源码分析(十) 提到过这个函数:

request({http_request, Method, {abs_path, AbsPath}, Version},
        State=#state{socket=Socket, transport=Transport,
        req_keepalive=Keepalive, max_keepalive=MaxKeepalive,
        onresponse=OnResponse, urldecode={URLDecFun, URLDecArg}=URLDec}) ->
    URLDecode = fun(Bin) -> URLDecFun(Bin, URLDecArg) end,
    {Path, RawPath, Qs} = cowboy_dispatcher:split_path(AbsPath, URLDecode),
    ConnAtom = if Keepalive < MaxKeepalive -> version_to_connection(Version);
        true -> close
    end,
    parse_header(#http_req{socket=Socket, transport=Transport,
        connection=ConnAtom, pid=self(), method=Method, version=Version,
        path=Path, raw_path=RawPath, raw_qs=Qs, onresponse=OnResponse,
        urldecode=URLDec}, State);

  就是在这里,我们设置的 #http_req.pid = self(),而我们在上面那个函数的最后,给这个处理请求的进程,发送了一个消息 {?MODULE, resp_sent},

  发送完消息后,上面那个函数返回 normal

  好了,今天的篇幅比较长,就到这吧,下一篇,我们看下,处理请求的进程如何处理这个消息,以及 Cowboy 源码分析(十七) 提到的cowboy_http_protocol:onrequest/2 函数。

  大家早点休息,晚安。