上一篇我們簡單介紹了 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,連夜解決了,今天在公司繼續盯着,找了點時間。寫完了這篇文章。