上一篇我们简单介绍了 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,连夜解决了,今天在公司继续盯着,找了点时间。写完了这篇文章。