代码改变世界

Cowboy 源码分析(四)

2012-05-19 17:52  rhinovirus  阅读(4112)  评论(3编辑  收藏  举报

  上一篇我们简单介绍了 cowboy 以及 cowboy_examples 下载,编译和运行,这篇我们来理解下 cowboy_examples 源码。

  1. 改造部分模块,使它符合OTP设计原则的应用,这点可能大家会比较疑惑,但是我之所以修改它,是为了大家更好的理解,我们都知道 OTP 应用(这里有点形式化,但是在初期方便新手找到入口),一般是三个文件,分别是 Application_app.erl,Application_sup.erl,和 Application.app.src(编译后为 Application.app)。但是我们看这个例子,并没有按照这种规范来命名,为了方便新手,我做出了下面几部分的修改:

  A. 首先,新增 cowboy_examples_app.erl 文件,拷贝cowboy_examples.erl 中的内容到这个文件,删除 start/0 方法,修改模块名:

-module(cowboy_examples_app).
-behaviour(application).
-export([start/2, stop/1]).

start(_Type, _Args) ->
    Dispatch = [
        {'_', [
            {[<<"websocket">>], websocket_handler, []},
            {[<<"eventsource">>], eventsource_handler, []},
            {[<<"eventsource">>, <<"live">>], eventsource_emitter, []},
            {'_', default_handler, []}
        ]}
    ],
    cowboy:start_listener(my_http_listener, 100,
        cowboy_tcp_transport, [{port, 8080}],
        cowboy_http_protocol, [{dispatch, Dispatch}]
    ),
    cowboy:start_listener(my_https_listener, 100,
        cowboy_ssl_transport, [
            {port, 8443}, {certfile, "priv/ssl/cert.pem"},
            {keyfile, "priv/ssl/key.pem"}, {password, "cowboy"}],
        cowboy_http_protocol, [{dispatch, Dispatch}]
    ),
    cowboy_examples_sup:start_link().

stop(_State) ->
    ok.

  B. 删除 cowboy_examples.erl 模块中的 stop/1 和 start/2 方法,增加 stop/0 方法,具体代码如下:

-module(cowboy_examples).
-export([start/0, stop/0]).

start() ->
    application:start(crypto),
    application:start(public_key),
    application:start(ssl),
    application:start(cowboy),
    application:start(cowboy_examples).

stop() ->
    application:stop(cowboy_examples).

  C. 修改 cowboy_examples_app.src 文件中应用的模块名,代码如下:

{application, cowboy_examples, [
    {description, "Examples for cowboy."},
    {vsn, "0.1.0"},
    {modules, []},
    {registered, [cowboy_examples_sup]},
    {applications, [
        kernel,
        stdlib,
        crypto,
        public_key,
        ssl,
        cowboy
    ]},
    {mod, {cowboy_examples_app, []}},
    {env, []}
]}.

  好了,经过上面的小改造,这个例子,已经符合 OTP 设计原则中的应用规范了。打开终端,我们重新编译下:

  cd ~/Source/cowboy_examples

  make clean

  make

  如下图:

  

  启动:

  终端输入:sh start.sh

  

  到这里,我们的小改造算是完成了。接下来,我们回到源码分析上。

  2. cowboy_examples_app 详解,调用 application:start(cowboy_examples). 时,会调用该模块中 start/2 方法,代码如下:

start(_Type, _Args) ->
    Dispatch = [
        {'_', [
            {[<<"websocket">>], websocket_handler, []},
            {[<<"eventsource">>], eventsource_handler, []},
            {[<<"eventsource">>, <<"live">>], eventsource_emitter, []},
            {'_', default_handler, []}
        ]}
    ],
    cowboy:start_listener(my_http_listener, 100,
        cowboy_tcp_transport, [{port, 8080}],
        cowboy_http_protocol, [{dispatch, Dispatch}]
    ),
    cowboy:start_listener(my_https_listener, 100,
        cowboy_ssl_transport, [
            {port, 8443}, {certfile, "priv/ssl/cert.pem"},
            {keyfile, "priv/ssl/key.pem"}, {password, "cowboy"}],
        cowboy_http_protocol, [{dispatch, Dispatch}]
    ).
    %%cowboy_examples_sup:start_link().

  我们发现,cowboy_examples_sup:start_link().启动的监控进程是没有用的,在这个例子。所以在这里我把它注释了,其实在这里也是不规范的,先不管了。

  我们看下上面代码 default_handler 模块,这个模块就是 http://localhost:8080/ 时响应 Hello world! 的 handler,代码如下:

-module(default_handler).
-behaviour(cowboy_http_handler).
-export([init/3, handle/2, terminate/2]).

init({_Any, http}, Req, []) ->
    {ok, Req, undefined}.

handle(Req, State) ->
    {ok, Req2} = cowboy_http_req:reply(200, [], <<"Hello world!">>, Req),
    {ok, Req2, State}.

terminate(_Req, _State) ->
    ok.

  注意,上面标红颜色的cowboy_http_handler,可能你也跟我一样,很好奇,没有见过 -behaviour(自定义行为)。我把它称为 自定义行为,其实这中用法,有点类似面向对象中的接口,如果你的模块中增加 -behaviour(自定义行为),那么该模块需要实现 自定义行为中指定的方法。我们来看下cowboy_http_handler 这个模块,它在 cowboy 源码中,如下:

-module(cowboy_http_handler).

-export([behaviour_info/1]).

%% @private
-spec behaviour_info(_)
    -> undefined | [{handle, 2} | {init, 3} | {terminate, 2}, ...].
behaviour_info(callbacks) ->
    [{init, 3}, {handle, 2}, {terminate, 2}];
behaviour_info(_Other) ->
    undefined.

  哇,是不是很简单,短短几行,behaviour_info(callbacks)这个方法,定义了,实现自定义行为的模块,需要实现的方法,我们可以看到 [{init, 3}, {handle, 2}, {terminate, 2}];分别是[{方法名, 参数个数}...] 这样的格式来规范,看明白了 cowboy_http_handler,我们接着回到 default_handler 模块。

  init/3 和 terminate/2 这两个方法比较简单,不介绍了;

  重点看下 handle/2 这个方法,如下:

  handle(Req, State) ->

    {ok, Req2} = cowboy_http_req:reply(200, [], <<"Hello world!">>, Req),

    {ok, Req2, State}.

  这个就是当我们访问 http://localhost:8080/ 返回 Hello world! 的处理,我们暂时先不管。

  回到 cowboy_examples_app 的 start/2 函数,我们看下这段代码:

%% @doc Start a listener for the given transport and protocol.
%%
%% A listener is effectively a pool of <em>NbAcceptors</em> acceptors.
%% Acceptors accept connections on the given <em>Transport</em> and forward
%% requests to the given <em>Protocol</em> handler. Both transport and protocol
%% modules can be given options through the <em>TransOpts</em> and the
%% <em>ProtoOpts</em> arguments. Available options are documented in the
%% <em>listen</em> transport function and in the protocol module of your choice.
%%
%% All acceptor and request processes are supervised by the listener.
%%
%% It is recommended to set a large enough number of acceptors to improve
%% performance. The exact number depends of course on your hardware, on the
%% protocol used and on the number of expected simultaneous connections.
%%
%% The <em>Transport</em> option <em>max_connections</em> allows you to define
%% the maximum number of simultaneous connections for this listener. It defaults
%% to 1024. See <em>cowboy_listener</em> for more details on limiting the number
%% of connections.
%%
%% Although Cowboy includes a <em>cowboy_http_protocol</em> handler, other
%% handlers can be created for different protocols like IRC, FTP and more.
%%
%% <em>Ref</em> can be used to stop the listener later on.
-spec start_listener(any(), non_neg_integer(), module(), any(), module(), any())
    -> {ok, pid()}.
start_listener(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts)
        when is_integer(NbAcceptors) andalso is_atom(Transport)
        andalso is_atom(Protocol) ->
    supervisor:start_child(cowboy_sup, child_spec(Ref, NbAcceptors,
        Transport, TransOpts, Protocol, ProtoOpts)).

%% @doc Return a child spec suitable for embedding.
%%
%% When you want to embed cowboy in another application, you can use this
%% function to create a <em>ChildSpec</em> suitable for use in a supervisor.
%% The parameters are the same as in <em>start_listener/6</em> but rather
%% than hooking the listener to the cowboy internal supervisor, it just returns
%% the spec.
-spec child_spec(any(), non_neg_integer(), module(), any(), module(), any())
    -> supervisor:child_spec().
child_spec(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts)
        when is_integer(NbAcceptors) andalso is_atom(Transport)
        andalso is_atom(Protocol) ->
    {{cowboy_listener_sup, Ref}, {cowboy_listener_sup, start_link, [
        NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts
    ]}, permanent, 5000, supervisor, [cowboy_listener_sup]}.

  该方法描述翻译如下(很差劲的翻译):

  开启一个指定传输协议的监听。

  NbAcceptors是一个有效的监听接收器。接收器接受连接在给定的 Transport和发送请求到 Protocol handler。通过 TransOpts 和 ProtoOpts 参数可以给  transport 和 protocol 两个模块指定选择项。在你选择的 protocol 模块,通过有效的选项监听传输方法。

  通过 listener 监督所有的接收器和请求进程。

  这是建立在足够大的数量来接收以提高性能。确切的数量当然取决于你的硬件。在一些预期的同时连接使用该协议

  在Transport中配置 max_connections 定义同时连接到 listener 的最大数量。默认值是 1024,查看 cowboy_listener 获得更多详细的连接数限制。

  虽然 Cowboy 包含一个 cowboy_http_protocol handler,更多类似 IRC, FTP 等不同协议的handlers可以创建。

  Ref 用来在之后用开停止 listener

  很抱歉,翻译的很烂。

  这个函数的参数类型说明:

  -spec start_listener(any(), non_neg_integer(), module(), any(), module(), any())

  该例子具体给的参数如下:

  Ref = my_http_listener

  NbAcceptors = 100

  Transport = cowboy_tcp_transport

  TransOpts = [{port, 8080}]

  Protocol = cowboy_http_protocol

  ProtoOpts = [{dispatch, Dispatch}]

  这个方法里就一句话,supervisor:start_child这个方法用于给一个存在的督程动态添加子进程,也就是给 cowboy_sup这个监督进程,动态添加子对象,而 child_spec这个方法是对子进程的定义,如果你看过我之前的几篇文章,其中一篇,提高很详细的 OTP设计原则的子进程规格,这个方法就是构造这样的规格。

  其实就是往 cowboy_sup 这个督程下,添加一个名为 cowboy_listener_sup 的督程:

  Id{cowboy_listener_sup, Ref}

  StartFunc = {M, F, A} = {cowboy_listener_sup, start_link, [NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts]}

  Restart = permanent

  Shutdown = 5000

  Type = supervisor

  Modules = [cowboy_listener_sup]

  好了,这个方法算是讲完了,当我们调用 cowboy_listener_sup:start_link([NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts]).传递了这些参数。那么接下来,我们应该关注这个 cowboy_listener_sup这个模块,它究竟是怎么跟自定义的协议等等关联的呢。

  关注我的下一篇文章吧。

  很抱歉,这篇文章拖了好几天,最近我们公司的游戏昨天刚上线就遇到了,接近宕机的bug,连夜解决了,今天在公司继续盯着,找了点时间。写完了这篇文章。