代码改变世界

Cowboy 源码分析(二)

2012-05-16 00:13  rhinovirus  阅读(6201)  评论(1编辑  收藏  举报

  大家好,在上一篇文章中,我们简单介绍了cowboy的其中2个文件,分别是 cowboy.app.src 应用程序资源文件(配置文件) 和 cowboy_app.erl 文件,今天,我们继续往下走,昨天的 cowboy_app.erl 中start/2 方法中有这一句,cowboy_sup:start_link(). 那么我们接下来就看下这个module。

  代码如下:

%% @private
-module(cowboy_sup).
-behaviour(supervisor).

-export([start_link/0]). %% API.
-export([init/1]). %% supervisor.

-define(SUPERVISOR, ?MODULE).

%% API.

-spec start_link() -> {ok, pid()}.
start_link() ->
    supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []).

%% supervisor.

-spec init([]) -> {'ok', {{'one_for_one', 10, 10}, [{
    any(), {atom() | tuple(), atom(), 'undefined' | [any()]},
    'permanent' | 'temporary' | 'transient',
    'brutal_kill' | 'infinity' | non_neg_integer(),
    'supervisor' | 'worker',
    'dynamic' | [atom() | tuple()]}]
}}.
init([]) ->
    Procs = [{cowboy_clock, {cowboy_clock, start_link, []},
        permanent, 5000, worker, [cowboy_clock]}],
    {ok, {{one_for_one, 10, 10}, Procs}}.

   如果你看了,我推荐的 Erlang OTP 设计原则,那么这个module一定不会陌生,它是Supervisor行为的实现,也就是启动一个监督进程,下面大概看下 Supervisor的

简单介绍(摘抄自Erlang OTP 设计原则):

  督程负责启动、停止和监视它的子进程。督程的基本思想是它要保持它的子进程有效,必要的时候可以重启他们。

  要启动和监视的子进程由一个 子进程规格 的列表来指定。子进程按照在这个列表中的顺序启动,并且按照相反的顺序终止。

  当我们 supervisor:start_link({local, ?SUPERVISOR}, ?MODULE, []). 这个监督进程的内部会调用 init/1 方法,这个方法需要 需要返回一个元组,我把 OTP 设计原则

里的详细描述贴过来了。这边对返回的一些字段的作用解释了相当清楚,如下:

  {Id, StartFunc, Restart, Shutdown, Type, Modules}
    Id = term()
    StartFunc = {M, F, A}
      M = F = atom()
      A = [term()]
    Restart = permanent | transient | temporary
    Shutdown = brutal_kill | integer() >=0 | infinity
    Type = worker | supervisor
    Modules = [Module] | dynamic
      Module = atom()

  • Id 是督程内部用于标识子进程规范的名称。
  • StartFunc 定义了用于启动子进程的很难书调用。它是一个模块.函数.参数的元组,与 apply(M, F, A) 用的一样。
  • Restart定义了一个被终止的子进程要在何时被重启:
    • permanent 子进程总会被重启。
    • temporary 子进程从不会被重启。
    • transient 子进程只有当其被异常终止时才会被重启,即,退出理由不是 normal
  • Shutdown定义了一个子进程应如何被终止。
    • brutal_kill 表示子进程应使用 exit(Child, kill) 进行无条件终止。
    • 一个整数超时值表示督程先通过调用 exit(Child, shutdown) 告诉子进程要终止了,然后等待其返回退出信号。如果在指定的事件内没有接受到任何退出信号,那么使用 exit(Child, kill) 无条件终止子进程。
    • 如果子进程是另外一个督程,那么应该设置为 infinity 以给予子树足够的时间关闭。
  • Type 指定子进程是督程还是佣程。
  • Modules 应该为只有一个元素的列表 [Module],其中 Module 是回调模块的名称,如果子进程是督程、gen_server或者gen_fsm。如果子进程是一个gen_event,那么 Modules 应为 dynamic 。 在升级和降级过程中发布处理器将用到这个信息,参见 发布处理

  不知道大家能不能看明白,从 cowboy_sup.erl

  Procs = [{cowboy_clock, {cowboy_clock, start_link, []},
  permanent, 5000, worker, [cowboy_clock]}],
  {ok, {{one_for_one, 10, 10}, Procs}}.
  我们现在应该能够理解这三行代码的意思,其实就是在 cowboy_sup 监控进程树下,有个模块cowboy_clock,当应用程序启动时,这个监控进程树就会启动,然后在这棵树下,启动一个模块,就是 cowboy_clock
  
  cowboy_clock 是 gen_server行为的一个模块,也就是客户端-服务器端(C/S)模型。完整代码如下:
%% @doc Date and time related functions.
%%
%% While a gen_server process runs in the background to update
%% the cache of formatted dates every second, all API calls are
%% local and directly read from the ETS cache table, providing
%% fast time and date computations.
-module(cowboy_clock).
-behaviour(gen_server).

-export([start_link/0, stop/0, rfc1123/0, rfc2109/1]). %% API.
-export([init/1, handle_call/3, handle_cast/2,
    handle_info/2, terminate/2, code_change/3]). %% gen_server.

-record(state, {
    universaltime = undefined :: undefined | calendar:datetime(),
    rfc1123 = <<>> :: binary(),
    tref = undefined :: undefined | timer:tref()
}).

-define(SERVER, ?MODULE).
-define(TABLE, ?MODULE).

-include_lib("eunit/include/eunit.hrl").

%% API.

%% @private
-spec start_link() -> {ok, pid()}.
start_link() ->
    gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

%% @private
-spec stop() -> stopped.
stop() ->
    gen_server:call(?SERVER, stop).

%% @doc Return the current date and time formatted according to RFC-1123.
%%
%% This format is used in the <em>'Date'</em> header sent with HTTP responses.
-spec rfc1123() -> binary().
rfc1123() ->
    ets:lookup_element(?TABLE, rfc1123, 2).

%% @doc Return the current date and time formatted according to RFC-2109.
%%
%% This format is used in the <em>'Set-Cookie'</em> header sent with
%% HTTP responses.
-spec rfc2109(calendar:datetime()) -> binary().
rfc2109(LocalTime) ->
    {{YYYY,MM,DD},{Hour,Min,Sec}} =
    case calendar:local_time_to_universal_time_dst(LocalTime) of
        [Gmt]   -> Gmt;
        [_,Gmt] -> Gmt;
        [] ->
            %% The localtime generated by cowboy_cookies may fall within
            %% the hour that is skipped by daylight savings time. If this
            %% is such a localtime, increment the localtime with one hour
            %% and try again, if this succeeds, subtracting the max_age
            %% from the resulting universaltime and converting to a local
            %% time will yield the original localtime.
            {Date, {Hour1, Min1, Sec1}} = LocalTime,
            LocalTime2 = {Date, {Hour1 + 1, Min1, Sec1}},
            case calendar:local_time_to_universal_time_dst(LocalTime2) of
                [Gmt]   -> Gmt;
                [_,Gmt] -> Gmt
            end
    end,
    Wday = calendar:day_of_the_week({YYYY,MM,DD}),
    DayBin = pad_int(DD),
    YearBin = list_to_binary(integer_to_list(YYYY)),
    HourBin = pad_int(Hour),
    MinBin = pad_int(Min),
    SecBin = pad_int(Sec),
    WeekDay = weekday(Wday),
    Month = month(MM),
    <<WeekDay/binary, ", ",
    DayBin/binary, " ", Month/binary, " ",
    YearBin/binary, " ",
    HourBin/binary, ":",
    MinBin/binary, ":",
    SecBin/binary, " GMT">>.

%% gen_server.

%% @private
-spec init([]) -> {ok, #state{}}.
init([]) ->
    ?TABLE = ets:new(?TABLE, [set, protected,
        named_table, {read_concurrency, true}]),
    T = erlang:universaltime(),
    B = update_rfc1123(<<>>, undefined, T),
    {ok, TRef} = timer:send_interval(1000, update),
    ets:insert(?TABLE, {rfc1123, B}),
    {ok, #state{universaltime=T, rfc1123=B, tref=TRef}}.

%% @private
-spec handle_call(_, _, State)
    -> {reply, ignored, State} | {stop, normal, stopped, State}.
handle_call(stop, _From, State=#state{tref=TRef}) ->
    {ok, cancel} = timer:cancel(TRef),
    {stop, normal, stopped, State};
handle_call(_Request, _From, State) ->
    {reply, ignored, State}.

%% @private
-spec handle_cast(_, State) -> {noreply, State}.
handle_cast(_Msg, State) ->
    {noreply, State}.

%% @private
-spec handle_info(_, State) -> {noreply, State}.
handle_info(update, #state{universaltime=Prev, rfc1123=B1, tref=TRef}) ->
    T = erlang:universaltime(),
    B2 = update_rfc1123(B1, Prev, T),
    ets:insert(?TABLE, {rfc1123, B2}),
    {noreply, #state{universaltime=T, rfc1123=B2, tref=TRef}};
handle_info(_Info, State) ->
    {noreply, State}.

%% @private
-spec terminate(_, _) -> ok.
terminate(_Reason, _State) ->
    ok.

%% @private
-spec code_change(_, State, _) -> {ok, State}.
code_change(_OldVsn, State, _Extra) ->
    {ok, State}.

%% Internal.

-spec update_rfc1123(binary(), undefined | calendar:datetime(),
    calendar:datetime()) -> binary().
update_rfc1123(Bin, Now, Now) ->
    Bin;
update_rfc1123(<< Keep:23/binary, _/bits >>,
        {Date, {H, M, _}}, {Date, {H, M, S}}) ->
    << Keep/binary, (pad_int(S))/binary, " GMT" >>;
update_rfc1123(<< Keep:20/binary, _/bits >>,
        {Date, {H, _, _}}, {Date, {H, M, S}}) ->
    << Keep/binary, (pad_int(M))/binary, $:, (pad_int(S))/binary, " GMT" >>;
update_rfc1123(<< Keep:17/binary, _/bits >>, {Date, _}, {Date, {H, M, S}}) ->
    << Keep/binary, (pad_int(H))/binary, $:, (pad_int(M))/binary,
        $:, (pad_int(S))/binary, " GMT" >>;
update_rfc1123(<< _:7/binary, Keep:10/binary, _/bits >>,
        {{Y, Mo, _}, _}, {Date = {Y, Mo, D}, {H, M, S}}) ->
    Wday = calendar:day_of_the_week(Date),
    << (weekday(Wday))/binary, ", ", (pad_int(D))/binary, Keep/binary,
        (pad_int(H))/binary, $:, (pad_int(M))/binary,
        $:, (pad_int(S))/binary, " GMT" >>;
update_rfc1123(<< _:11/binary, Keep:6/binary, _/bits >>,
        {{Y, _, _}, _}, {Date = {Y, Mo, D}, {H, M, S}}) ->
    Wday = calendar:day_of_the_week(Date),
    << (weekday(Wday))/binary, ", ", (pad_int(D))/binary, " ",
        (month(Mo))/binary, Keep/binary,
        (pad_int(H))/binary, $:, (pad_int(M))/binary,
        $:, (pad_int(S))/binary, " GMT" >>;
update_rfc1123(_, _, {Date = {Y, Mo, D}, {H, M, S}}) ->
    Wday = calendar:day_of_the_week(Date),
    << (weekday(Wday))/binary, ", ", (pad_int(D))/binary, " ",
        (month(Mo))/binary, " ", (list_to_binary(integer_to_list(Y)))/binary,
        " ", (pad_int(H))/binary, $:, (pad_int(M))/binary,
        $:, (pad_int(S))/binary, " GMT" >>.

%% Following suggestion by MononcQc on #erlounge.
-spec pad_int(0..59) -> binary().
pad_int(X) when X < 10 ->
    << $0, ($0 + X) >>;
pad_int(X) ->
    list_to_binary(integer_to_list(X)).

-spec weekday(1..7) -> <<_:24>>.
weekday(1) -> <<"Mon">>;
weekday(2) -> <<"Tue">>;
weekday(3) -> <<"Wed">>;
weekday(4) -> <<"Thu">>;
weekday(5) -> <<"Fri">>;
weekday(6) -> <<"Sat">>;
weekday(7) -> <<"Sun">>.

-spec month(1..12) -> <<_:24>>.
month( 1) -> <<"Jan">>;
month( 2) -> <<"Feb">>;
month( 3) -> <<"Mar">>;
month( 4) -> <<"Apr">>;
month( 5) -> <<"May">>;
month( 6) -> <<"Jun">>;
month( 7) -> <<"Jul">>;
month( 8) -> <<"Aug">>;
month( 9) -> <<"Sep">>;
month(10) -> <<"Oct">>;
month(11) -> <<"Nov">>;
month(12) -> <<"Dec">>.

%% Tests.

-ifdef(TEST).

update_rfc1123_test_() ->
    Tests = [
        {<<"Sat, 14 May 2011 14:25:33 GMT">>, undefined,
            {{2011, 5, 14}, {14, 25, 33}}, <<>>},
        {<<"Sat, 14 May 2011 14:25:33 GMT">>, {{2011, 5, 14}, {14, 25, 33}},
            {{2011, 5, 14}, {14, 25, 33}}, <<"Sat, 14 May 2011 14:25:33 GMT">>},
        {<<"Sat, 14 May 2011 14:25:34 GMT">>, {{2011, 5, 14}, {14, 25, 33}},
            {{2011, 5, 14}, {14, 25, 34}}, <<"Sat, 14 May 2011 14:25:33 GMT">>},
        {<<"Sat, 14 May 2011 14:26:00 GMT">>, {{2011, 5, 14}, {14, 25, 59}},
            {{2011, 5, 14}, {14, 26,  0}}, <<"Sat, 14 May 2011 14:25:59 GMT">>},
        {<<"Sat, 14 May 2011 15:00:00 GMT">>, {{2011, 5, 14}, {14, 59, 59}},
            {{2011, 5, 14}, {15,  0,  0}}, <<"Sat, 14 May 2011 14:59:59 GMT">>},
        {<<"Sun, 15 May 2011 00:00:00 GMT">>, {{2011, 5, 14}, {23, 59, 59}},
            {{2011, 5, 15}, { 0,  0,  0}}, <<"Sat, 14 May 2011 23:59:59 GMT">>},
        {<<"Wed, 01 Jun 2011 00:00:00 GMT">>, {{2011, 5, 31}, {23, 59, 59}},
            {{2011, 6,  1}, { 0,  0,  0}}, <<"Tue, 31 May 2011 23:59:59 GMT">>},
        {<<"Sun, 01 Jan 2012 00:00:00 GMT">>, {{2011, 5, 31}, {23, 59, 59}},
            {{2012, 1,  1}, { 0,  0,  0}}, <<"Sat, 31 Dec 2011 23:59:59 GMT">>}
    ],
    [{R, fun() -> R = update_rfc1123(B, P, N) end} || {R, P, N, B} <- Tests].

pad_int_test_() ->
    Tests = [
        { 0, <<"00">>}, { 1, <<"01">>}, { 2, <<"02">>}, { 3, <<"03">>},
        { 4, <<"04">>}, { 5, <<"05">>}, { 6, <<"06">>}, { 7, <<"07">>},
        { 8, <<"08">>}, { 9, <<"09">>}, {10, <<"10">>}, {11, <<"11">>},
        {12, <<"12">>}, {13, <<"13">>}, {14, <<"14">>}, {15, <<"15">>},
        {16, <<"16">>}, {17, <<"17">>}, {18, <<"18">>}, {19, <<"19">>},
        {20, <<"20">>}, {21, <<"21">>}, {22, <<"22">>}, {23, <<"23">>},
        {24, <<"24">>}, {25, <<"25">>}, {26, <<"26">>}, {27, <<"27">>},
        {28, <<"28">>}, {29, <<"29">>}, {30, <<"30">>}, {31, <<"31">>},
        {32, <<"32">>}, {33, <<"33">>}, {34, <<"34">>}, {35, <<"35">>},
        {36, <<"36">>}, {37, <<"37">>}, {38, <<"38">>}, {39, <<"39">>},
        {40, <<"40">>}, {41, <<"41">>}, {42, <<"42">>}, {43, <<"43">>},
        {44, <<"44">>}, {45, <<"45">>}, {46, <<"46">>}, {47, <<"47">>},
        {48, <<"48">>}, {49, <<"49">>}, {50, <<"50">>}, {51, <<"51">>},
        {52, <<"52">>}, {53, <<"53">>}, {54, <<"54">>}, {55, <<"55">>},
        {56, <<"56">>}, {57, <<"57">>}, {58, <<"58">>}, {59, <<"59">>}
    ],
    [{I, fun() -> O = pad_int(I) end} || {I, O} <- Tests].

-endif.

   我们看下关于这个模块的描述:

  %% @doc Date and time related functions.
  %%
  %% While a gen_server process runs in the background to update
  %% the cache of formatted dates every second, all API calls are
  %% local and directly read from the ETS cache table, providing
  %% fast time and date computations.
  -module(cowboy_clock).

  大概意思翻译:

  一个日期和时间关系的函数列表

  这个是我根据表面理解的翻译:

    有一个 gen_server 后台进程运行,时刻跟新格式化的日期缓存,倘若第一时间有日期的计算,所有的 api 调用,将从本地 ets 缓存表中读取。

  这个是让我朋友帮我翻译的:

    当一个时刻格式化日期缓存的gen_server后台进程运行时,所有API直接从Ets缓存调用读取,快速进行时间和日期计算

  唉,这说明我E文实在很烂,献丑了。

  好了,cowboy_clock 这个模块应该是用于计算,日期和时间相关的函数列表。

  下面我们详细介绍下这个模块里的内容:

  1. 首先是 module 定义,以及 一些函数导出,这个我就不介绍了,如果连这都看不懂,建议看看 Erlang程序设计 这本书,或者 Erlang 编程指南,或者官方的新手教程。

  2. 下面这几行是定义记录,记录名为 state, 注意下,= 后面是 记录字段类型的描述

    -record(state, {
        universaltime = undefined :: undefined | calendar:datetime(),
        rfc1123 = <<>> :: binary(),
        tref = undefined :: undefined | timer:tref()
    }).  

  3. 宏定义:

    -define(SERVER, ?MODULE).  %%服务名
    -define(TABLE, ?MODULE). %%ets 表名

  4. 单元测试相关,这个我打算先跳过,以后有机会,再写一篇单元测试相关的文章,还有 上一篇文章中提到的 eprof 提供函数运行时间的百分比,我以后会专门写文章介绍。

    -include_lib("eunit/include/eunit.hrl").

    还有下面这行往下的所有代码,咱们都跳过:

    -ifdef(TEST).

  5. 启动这个gen_server。注意上面这行是啥意思呢,它是文档标记语言,用户描述生成文档用的,咱们看是 start_link()这个方法返回{ok, pid()} 这个结果。

    -spec start_link() -> {ok, pid()}.
    start_link() ->
      gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).

  6. 停止这个gen_server

    -spec stop() -> stopped.
    stop() ->
      gen_server:call(?SERVER, stop).

  7. rfc1123/0方法

    Return the current date and time formatted according to RFC-1123.

    返回当前日期和时间,格式化为RFC-1123这样的表示方法, 大概就是这样表示日期: ddd,dd MMM yyyy,HH':'mm':'ss 'GMT'。

    ets:lookup_element 这个是从 ?TABLE ets 表里取 rfc1123 这样值。

    -spec rfc1123() -> binary().
    rfc1123() ->
      ets:lookup_element(?TABLE, rfc1123, 2).

  8. rfc2109/1 方法,RFC2109规范也是一种规范。

    Return the current date and time formatted according to RFC-2109.

    返回当前日期和时间,格式化为 RFC-2109这样的表示方法

    我们注意下面两行注释,大概意思就是 这种格式化,用于 发送 http responses 头部 'Set-Cookie' 中.

    %% This format is used in the <em>'Set-Cookie'</em> header sent with
    %% HTTP responses.

  9. init/1 gen_server的初始化方法

    -spec init([]) -> {ok, #state{}}.
      init([]) ->
        ?TABLE = ets:new(?TABLE, [set, protected,
                  named_table, {read_concurrency, true}]),
        T = erlang:universaltime(),
        B = update_rfc1123(<<>>, undefined, T),
        {ok, TRef} = timer:send_interval(1000, update),
        ets:insert(?TABLE, {rfc1123, B}),
        {ok, #state{universaltime=T, rfc1123=B, tref=TRef}}.

    这个方法会创建,一个 ets表,创建表,需要大家百度了解下,如何创建 ets 表,在这边不详细介绍了,篇幅有限。

    erlang:universaltime/0 这个方法是返回当前系统时间;

    这个方法是把 当前系统时间 返回 rfc1123 的二进制格式, 大家看下面 update_rfc1123 这些方法的具体逻辑,不一一介绍了;

    timer:send_interval/2 这个方法是定义一个定时器,每个1000毫秒,发送一条消息 "update" 给 self(),这里就是gen_server进程,我们看handle_info有接受update消息的处理代码;

    ets:insert/2 插入ets表数据;

    最后一行是返回 State。

  10. handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/2 这几个方法属于 gen_server 行为管用方法,具体惨看 Erlang OTP 设计原则.

    其中有这一行, {ok, cancel} = timer:cancel(TRef), 取消定时器;

    跟新 ets 中 当前时间 rtf1123 格式字符串

    T = erlang:universaltime(),
    B2 = update_rfc1123(B1, Prev, T),
    ets:insert(?TABLE, {rfc1123, B2}),
    {noreply, #state{universaltime=T, rfc1123=B2, tref=TRef}};

  11. 剩下的方法 pad_int/1 这个方法是把 0 到 59 的数字转为 二进制格式;weekday/1 这个方法是返回星期的英文字母表现;month/1 这个是返回月份的英文字母表现;

  到目前为止,cowboy_clock 模块中的所有方法我们都详细介绍了,再下一篇中,我会运行 cowboy的一个例子,也就是在上一篇提到的那个例子,可能你会比较疑惑,为什么 cowboy 应用程序到目前为止,只启动了一个跟日期和时间相关的模块,没有涉及别的模块呢,下回再分解,谢谢。

  附上官方 timer 模块说明 http://www.erlang.org/doc/man/timer.html