cowboy源碼分析


 2013-01-21 by 謝鴻鋒  

原創文章,轉載請注明:轉載自Erlang雲中漫步 

目錄

=================================

一、概述

二、ranch源碼分析

三、cowboy源碼分析

   1、Request調度規則

   2、http協議實現分析

   3、http協議之chunked編碼

   4、http協議之long_polling

   5、http協議之websocket

   6、http協議之rest-api

=================================

 

cowboy 越來越讓人舒服了,改版之后的cowboy分為兩大application,將TCP拆分出來,成了ranch application,cowboy成了基於TCP(ranch)的一個cowboy_protocol(http實現)。不僅如此,cowboy還給出了rest-api、websocket、chunked、long-polling的支持,相當之完美!

 

一、概述

cowboy是一個小型、快速,模塊化,采用Erlang開發的HTTP服務器。

ranch 是一個socket acceptor pool,TCP協議類型。

 

cowboy的特點:

1.代碼少。
2.速度快。
3.模塊化程度高,transport和protocol都可輕易替換。
4.采用二進制語法實現http服務,更快更小。
5.極易嵌入其它應用。
6.有dispatcher,可以嵌入FastCGI PHP 或者是 Ruby.
7.沒有進程字典,代碼干凈。

 

總體來講cowboy的特點在於分層架構及模塊化設計,即把網絡層的套接字管理和應用層協議實現,以及對消息的處理,這三層幾乎完全解藕。

 

cowboy application詳細介紹見:https://github.com/extend/cowboy/

{application, cowboy, [

      {id, "Cowboy"},

      {description, "Small, fast, modular HTTP server."},

      {sub_description, "Cowboy is also a socket acceptor pool, "

           "able to accept connections for any kind of TCP protocol."},

      {vsn, "0.7.0"},

      {applications, [

           kernel,

           stdlib,

           ranch,

           crypto

      ]},

      {mod, {cowboy_app, []}},

 

ranch application詳細介紹見:https://github.com/extend/ranch/

{application, ranch, [

      {id, "Ranch"},

      {description, "Socket acceptor pool for TCP protocols."},

      {sub_description, "Reusable library for building networked applications."},

      {vsn, "0.6.0"},

      {mod, {ranch_app, []}},

  

二、ranch源碼分析

 

1、看下效果

 

a) 啟動ranch應用 application:start(ranch).

 

b)啟動例子tcp_echo應用 application:start(tcp_echo).

 

c)客戶端連接測試

 

 

d)處理客戶端請求

 

e)客戶端斷開

 

 

下面對關鍵代碼執行軌跡進行分析

 

2、啟動ranch應用

> application:start(ranch).

 

代碼執行軌跡關注點見下面幾張圖紅線框住部分

 

  

 

 啟動了監督進程ranch_sup,以one_for_one方式監督啟動ranch_server工作進程

 

 

此時進程監督樹如下:

 

關注數據:etsranch_server,此時為空,

               ranch_server進程狀態數據state,此時為空,

下面跟蹤客戶端連接進來時,這兩個數據的變化情況。

 

3、啟動例子tcp_echo應用

> application:start(tcp_echo).

 代碼執行軌跡關注點見下面幾張圖紅線框住部分

 

 

 It will have a pool of 1 acceptors, use a TCP transport and forward connections to the “echo_protocol” handler.

 

 動態生成ranch_sup的子進程{ranch_listener_sup, Ref},類型為supervisor。Ref值為tcp_echo。

結束此進程可以調用ranch:stop_listener(tcp_echo)。ranch_sup在application:start(ranch)時已經產生。

 

各參數說明見其注釋:

Start a listener for the given transport and protocol.

A listener is effectively a pool of NbAcceptors acceptors. Acceptors accept connections on the given Transport and forward connections to the given Protocol handler. Both transport and protocol modules can be given options through the TransOpts and the ProtoOpts arguments. Available options are documented in the listen transport function and in the protocol module of your choice.

All acceptor and connection 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 Transport option max_connections allows you to define the maximum number of simultaneous connections for this listener. It defaults to 1024. See ranch_listener for more details on limiting the number of connections.

Ref can be used to stop the listener later on.

This function will return {error, badarg}` if and only if the transport module given doesnt appear to be correct.

 

 

 

 此時進程狀態數據及ets表數據如下:

 

ranch_server進程monitor進程<0.41.0>、<0.44.0>。<0.41.0>為listener,<0.44.0>為acceptor。<0.42.0>為connections 管理者,客戶端連接進程由其以simple_one_for_one方式監控。此時客戶端連接數為0。

 

ets表及進程狀態數據生成跟蹤代碼軌跡:

 

 

至此,application(ranch)、application(tcp_echo)啟動完成,

進程監督樹產生/監督類型及關鍵代碼軌跡分析完畢。

進程狀態數據及ets表數據生成及關鍵代碼軌跡也已分析完畢。

 

下面跟蹤有客戶端連接時的情況。

 

 4、客戶端連接

 

 

 

接收到客戶端連接,從下面幾個方面進行分析

A、 生成ConnsSup子進程,controlling_process(CSocket, ConnPid)綁定接收的客戶端socket。

 

上圖<0.42.0>為ConnsSup,<0.78.0>為ConnPid

 

B、 ConnPid生成,代碼軌跡如下

 

 

C、 更新表ranch_server客戶端連接數

59行ranch_listener:add_connection(ListenerPid,ConnPid)更新客戶端連接數

 

 

D、 max_connections最大連接數處理:超過最大連接數將不再進行accept,輪詢檢測連接數,當小於最大連接數時,才開始accept接受客戶端的連接。

 

 5、處理客戶端請求

 

客戶端發的數據,服務器端收到后,原樣響應傳回給客戶端。

 

6、客戶端斷開。

 

 

到這里,ranch的源碼分析就完成了。

 

不足之處:acceptor接收客戶端連接,用了一個臨時的進程來中轉,中轉完畢此進程即銷毀。

 

上圖<0.79.0>即為臨時進程,其功能完全可以合並到<0.44.0>中來進行。何必“創建->中轉->銷毀”多此一舉呢?hotwheels的處理就是如此,干脆利落,堪稱優雅。hotwheels源碼分析見博主另外一篇原創文章: http://www.cnblogs.com/poti/archive/2012/11/06/hotwheels.html

 

 三、cowboy源碼分析

 

cowboy application實現了http協議,給出了rest-api、websocket、chunked、long-polling的支持,相當完美。

 

1、Request調度規則

 

見cowboy_dispatcher:match(Dispatch, Host, Path)

 

-spec match(Dispatch::dispatch_rules(), Host::binary() | tokens(), Path::binary())

    -> {ok, module(), any(), bindings(),

      HostInfo::undefined | tokens(), PathInfo::undefined | tokens()}

    | {error, notfound, host} | {error, notfound, path}

    | {error, badrequest, path}.

 

-type tokens() :: [binary()].

-type match_rule() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()].

-type dispatch_path() :: [{match_rule(), module(), any()}].

-type dispatch_rule() :: {Host::match_rule(), Path::dispatch_path()}.

-type dispatch_rules() :: [dispatch_rule()].

  

示例說明:

match_test_() ->

    Dispatch = [

       {[<<"www">>, '_', <<"ninenines">>, <<"eu">>], [

           {[<<"users">>, '_', <<"mails">>], match_any_subdomain_users, []}

       ]},

       {[<<"ninenines">>, <<"eu">>], [

           {[<<"users">>, id, <<"friends">>], match_extend_users_friends, []},

           {'_', match_extend, []}

       ]},

       {[<<"ninenines">>, var], [

           {[<<"threads">>, var], match_duplicate_vars,

              [we, {expect, two}, var, here]}

       ]},

       {[<<"erlang">>, ext], [

           {'_', match_erlang_ext, []}

       ]},

       {'_', [

           {[<<"users">>, id, <<"friends">>], match_users_friends, []},

           {'_', match_any, []}

       ]}

    ],

    %% {Host, Path, Result}

    Tests = [

       {<<"any">>, <<"/">>, {ok, match_any, [], []}},

       {<<"www.any.ninenines.eu">>, <<"/users/42/mails">>,

           {ok, match_any_subdomain_users, [], []}},

       {<<"www.ninenines.eu">>, <<"/users/42/mails">>,

           {ok, match_any, [], []}},

       {<<"www.ninenines.eu">>, <<"/">>,

           {ok, match_any, [], []}},

       {<<"www.any.ninenines.eu">>, <<"/not_users/42/mails">>,

           {error, notfound, path}},

       {<<"ninenines.eu">>, <<"/">>,

           {ok, match_extend, [], []}},

       {<<"ninenines.eu">>, <<"/users/42/friends">>,

           {ok, match_extend_users_friends, [], [{id, <<"42">>}]}},

       {<<"erlang.fr">>, '_',

           {ok, match_erlang_ext, [], [{ext, <<"fr">>}]}},

       {<<"any">>, <<"/users/444/friends">>,

           {ok, match_users_friends, [], [{id, <<"444">>}]}},

       {<<"ninenines.fr">>, <<"/threads/987">>,

           {ok, match_duplicate_vars, [we, {expect, two}, var, here],

              [{var, <<"fr">>}, {var, <<"987">>}]}}

    ],

    [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->

       {ok, Handler, Opts, Binds, undefined, undefined}

           = match(Dispatch, H, P)

    end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests].

 

match_info_test_() ->

    Dispatch = [

       {[<<"www">>, <<"ninenines">>, <<"eu">>], [

           {[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], match_path, []}

       ]},

       {['...', <<"ninenines">>, <<"eu">>], [

           {'_', match_any, []}

       ]}

    ],

    Tests = [

       {<<"ninenines.eu">>, <<"/">>,

           {ok, match_any, [], [], [], undefined}},

       {<<"bugs.ninenines.eu">>, <<"/">>,

           {ok, match_any, [], [], [<<"bugs">>], undefined}},

       {<<"cowboy.bugs.ninenines.eu">>, <<"/">>,

           {ok, match_any, [], [], [<<"cowboy">>, <<"bugs">>], undefined}},

       {<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>,

           {ok, match_path, [], [], undefined, []}},

       {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>,

           {ok, match_path, [], [], undefined, [<<"path_info">>]}},

       {<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>,

           {ok, match_path, [], [], undefined, [<<"foo">>, <<"bar">>]}}

    ],

    [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->

       R = match(Dispatch, H, P)

    end} || {H, P, R} <- Tests].

 

2、http協議分析

 

http協議說明見:http://www.cnblogs.com/poti/articles/2851330.html

 

從http_SUITE: echo_body/1例子開始分析http協議解析。

 

命令行代碼執行:

1> Config = [{priv_dir,"D:/eclipse/workspace/cowboy/test/priv"}].

2> http_SUITE:init_per_suite(Config).

3> Client = http_SUITE:init_per_group(http, Config).

4> http_SUITE:echo_body(Client).

{ok,<<"aaa">>,

    {client,request,[],#Port<0.1848>,ranch_tcp,5000,<<>>,

            keepalive,

            {1,1},

            undefined}}

 

調度規則:

Dispatch =

    [

        {[<<"localhost">>], [

            {[<<"echo">>, <<"body">>], http_handler_echo_body, []},

        ]}

    ]

 

測試代碼:

 

請求處理模塊

 

 以上代碼完成了下面功能:

A、客戶端http請求

 

B、服務器http響應

 

 

cowboy_protocol.erl代碼分析

 parse_method:解析method。 例子中是POST

parse_uri_path:解析path。   /echo/body

parse_version:解析version。  HTTP/1.1

parse_uri_query:解析query,url中$?分割部分。 例子中為空。

parse_uri_fragment:解析fragment,url中$#分割部分。 例子中為空。

parse_header:解析header。

Headers = [{<<"connection">>,<<"close">>},

             {<<"user-agent">>,<<"Cow">>},

             {<<"host">>,<<"localhost:33080">>},

             {<<"content-length">>,<<"3">>}]

parse_host:解析host。 { <<"localhost">>, 33080 }

request:開始處理request。

 

 

 cowboy_router.erl作用:根據Reqeust從Dispatch中找到handler模塊及handle_opts。

 

 

cowboy_handler.erl作用:執行handler模塊,帶上參數handler_opts。

 

接下來,看Handler:init/3、Handler:handle/2,這里Handler為http_handler_echo_body.erl。

 

init/3的結果HandlerState這里為undefined會傳給handle/2參數State。

到這里,客戶端http請求處理完畢,

 

服務器http響應完成后將根據request中connection字段的值進行相應處理。

如果connection=close則Transport:close(Socket)斷開連接;

如果connection=keep-alive則保持連接,處理下一個請求;如果超時未收到請求,則斷開連接。

 

3、http協議chunked編碼

 

chunked編碼是HTTP/1.1 RFC里定義的一種編碼方式,

協議說明見:http://www.cnblogs.com/poti/articles/2822159.html

 

從http_SUITE: chunked_response/1例子開始進行分析

 

客戶端http請求:

服務器http響應:

 

 

服務器http響應,分下面幾個數據包進行:

a)cowboy_req:chunked_reply(200, Req) -> chunked協議http頭部發送

 

關鍵代碼軌跡如下:

 

b)cowboy_req:chunk("chunked_handler\r\n", Req2) -> 第1個chunk數據包發送。

發送chunk數據包chunked_handler\r\n

 

關鍵代碼軌跡如下:

 

c)cowboy_req:chunk("works fine!", Req2) -> 第2個chunk數據包發送。

發送chunk數據包works fine!。分析同b),不贅述。

 

d)cowboy_req:ensure_response(#http_req{socket=Socket, transport=Transport,resp_state=chunks}, _) -> 最后一個chunk數據包發送。

發送last-chunk數據包<<"0\r\n\r\n">>

  

4、http協議long_polling

 

long-polling的服務,其客戶端是不做輪詢的,客戶端在發起一次請求后立即掛起,一直到服務器端有更新的時候,服務器才會主動推送信息到客戶端。 在服務器端有更新並推送信息過來之前這個周期內,客戶端不會有新的多余的請求發生,服務器端對此客戶端也啥都不用干,只保留最基本的連接信息,一旦服務器有更新將推送給客戶端,客戶端將相應的做出處理,處理完后再重新發起下一輪請求。

 

從http_SUITE: check_status/1例子開始進行分析

 

 

/long_polling 處理模塊為http_handler_long_polling

 

 定時器動作5次后給客戶端響應狀態碼102。下面分析下服務器代碼處理過程:

 

 erlang:send_after/3、erlang:start_timer/3說明見:http://www.cnblogs.com/poti/articles/2823209.html

 

cowboy_handler:handler_init/4執行后返回結果為

 

 

 

 

 erlang:hibernate/3用途:使當前進程進入休眠狀態,當有消息發送給進程時,激活進程調用Module:Function(Args)。

 這里Module為cowboy_protocol,Function為resume。

 這個例子中激活進程的消息從何而來?兩個地方:

 

激活進程后執行方法:cowboy_protocol:resume/6

 

這里Module為cowboy_handler,Function為handler_loop。接下來的代碼執行軌跡如下:

 

當前進程再次進入休眠狀態,這里Module為cowboy_handler,Function為handler_loop。

激活進程后執行方法:cowboy_handler: handler_loop

直到計數器為0

 

 以上就是cowboy中long-polling的處理過程。

 

小結一下此例子中cowboy的long-polling處理過程:

a)  處理模塊init方法啟動一個定時器,同時返回結果: {loop, Req, state(), timeout(), hibernate}

b)  當前進程進入休眠狀態

c)  定時器發送消息,激活休眠的進程,回調處理模塊的方法info/3

d)  如果服務器響應客戶端的條件符合,則服務器給客戶端響應結果,到這里,客戶端的長連接就處理完畢了

e)  否則,info/3中重新啟動一個定時器,goto 到b)繼續執行。

 

cowboy中實現long-polling的三種方式:

 

init/3 -> {loop, Req, state(), hibernate}

當前進程不創建定時器,有休眠狀態。不限時的休眠方式長輪詢。

 

init/3 -> {loop, Req, state(), timeout()}

當前進程創建定時器,無休眠狀態。限時的無休眠方式長輪詢。

 

init/3 -> {loop, Req, state(), timeout(), hibernate}

當前進程創建定時器,有休眠狀態。限時且有休眠方式的長輪詢。

 

hibernate的作用:進程進入休眠狀態,消耗服務器資源最小化,直到有消息到達時被激活。

 

5、http協議websocket

 

5.1、websocket草案00協議 見:http://www.cnblogs.com/poti/articles/2828392.html

 

cowboy中websocket版本00的實現過程如下:

a)  握手協議,建立websocket連接通道

客戶端發送消息:

 

請求中的“Sec-WebSocket-Key1”,“Sec-WebSocket-Key2”和最后的“8字節的Key3”都是隨機的,

服務器端會用這些數據來構造出一個16字節的應答。

其中:8字節的Key3為請求的內容,其它的都是http請求頭。

判斷當前請求是否WEBSOCKET,主要是通過請求頭中的Connection是不是等於Upgrade以及Upgrade是否等於WebSocket。

 

服務器響應消息:

 

把請求的第一個Key中的數字除以第一個Key的空白字符的數量,而第二個Key也是如此,然后把這兩個結果與請求最后的8字節字符串連接起來,然后進行MD5構造產生16字節的加密數據。

 

b)  消息傳送

客戶端和服務端發送非握手文本消息,消息以0x00開頭,0xFF結尾。

 

c)  連接斷開

客戶端發送<<0xFF, 0x00>>,服務器回復<<0xFF, 0x00>>。

 

下面對關鍵代碼進行分析

 

a 握手協議

a.1 客戶端發送:

 

 

 a.2 服務器處理:

處理模塊websocket_handler.erl:init/3返回結果

{upgrade, protocol, cowboy_websocket}.

 

 

 http請求頭部驗證:下圖紅線框住部分必須要有。

 

 

服務器給客戶端發送:

 

 

status(101) -> <<"101 Switching Protocols">>;切換協議。

 

a.3 客戶端收到后,如下處理:

 

101狀態檢查。http響應頭部驗證:下圖紅線框住部分必須要有。

 

 接着,客戶端往服務器發送一個隨機的8個字節的字符串給服務器。

 

 

a.4 服務器收到此數據,作為key3,與前面的

Sec-Websocket-Key1: Y\" 4 1Lj!957b8@0H756!i

Sec-Websocket-Key2: 1711 M;4\\74  80<6

一起生成Challenge(16個字節的加密KEY),加密KEY算法:

Sec_WebSocket-Key1的產生方式:
(1)提取客戶端請求的Sec_WebSocket-Key1中的數字字符組成字符串k1
(2)轉換字符串k1為8個字節的長整型intKey1
(3)統計客戶端請求的Sec_WebSocket-Key1中的空格數k1Spaces
(4)intKey1/k1Spaces取整k1FinalNum
(5)將k1FinalNum轉換成字節數組再反轉最終形成4個字節的Sec_WebSocket-Key1

Sec_WebSocket-Key2的產生方式:
(1)提取客戶端請求的Sec_WebSocket-Key2中的數字字符組成字符串k2
(2)轉換字符串k2為8個字節的長整型intKey2
(3)統計客戶端請求的Sec_WebSocket-Key2中的空格數k2Spaces
(4)intKey2/k2Spaces取整k2FinalNum
(5)將k2FinalNum轉換成字節數組再反轉最終形成4個字節的Sec_WebSocket-Key2

Sec_WebSocket-Key3的產生方式:
客戶端握手請求的最后8個字節

將Sec_WebSocket-Key1、Sec_WebSocket-Key2、Sec_WebSocket-Key3合並成一個16字節數組
再進行MD5加密形成最終的16個字節的加密KEY

 

 

服務器生成Challenge后,發送給客戶端,

a.5 客戶端收到此消息后,握手協議到此就算完成。

 

b 握手協議完成后,進行消息的發送接收:

客戶端和服務端發送非握手文本消息,消息以0x00開頭,0xFF結尾。

服務器端:

 

客戶端:

 

 

c 斷開連接

客戶端發送<<0xFF, 0x00>>,服務器回復<<0xFF, 0x00>>。

客戶端:

 

服務器:

 

 

 

5.2、websocket草案10協議 見:http://www.cnblogs.com/poti/articles/2828378.html

 

cowboy中websocket版本7、8、13的實現過程:

a)  握手協議,建立websocket連接通道

客戶端發送消息:

 

1)Sec-WebSocket-Key后面的長度為24的字符串是客戶端隨機生成的,我們暫時叫他cli_key,服務器必須用它經過一定的運算規則生成服務器端的key,暫時叫做ser_key,然后把ser_key發回去,ser_key后面會介紹;

2)把http頭中Upgrade的值由"WebSocket"修改為了"websocket";

3)把http頭中的"Origin"修改為了"Sec-WebSocket-Origin";

4)增加了http頭"Sec-WebSocket-Accept",用來返回原來草案00服務器返回給客戶端的握手驗證,原來是以body的形式返回,現在是放到了http頭中。

 

服務器響應消息:

 

服務器端制作秘鑰ser_key:

1)服務器端將cli_key(長度24)截取出來dGhlIHNhbXBsZSBub25jZQ==

用它和自定義的一個字符串(長度36):258EAFA5-E914-47DA-95CA-C5AB0DC85B11 連接起來,像這樣:dGhlIHNhbXBsZSBub25jZQ==258EAFA5-E914-47DA-95CA-C5AB0DC85B11

2)然后把這一長串經過SHA-1算法加密,得到長度為20字節的二進制數據,再將這些數據經過Base64編碼,最終得到服務端的密鑰,也就是ser_key:s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

3)然后將ser_key發送給客戶端

 

至此,算是握手成功了!

 

b)  消息傳送

 

消息格式:

 

各字段詳細說明見:http://www.cnblogs.com/poti/articles/2828378.html

cowboy中將按這個規則對數據進行編碼及解碼。下面對Opcode做個說明:

Opcode:4位操作碼,定義有效負載數據,以下是定義的操作碼:

      *  %x0 表示連續消息片斷
      *  %x1 表示文本消息片斷
      *  %x2 表未二進制消息片斷
      *  %x3-7 為將來的非控制消息片斷保留的操作碼
      *  %x8 表示連接關閉
      *  %x9 表示心跳檢查的ping
      *  %xA 表示心跳檢查的pong
      *  %xB-F 為將來的控制消息片斷的保留操作碼

 

c)  心跳消息:

ok = gen_tcp:send(Socket, << 1:1, 0:3, 9:4, 0:8 >>), %% ping

{ok, << 1:1, 0:3, 10:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000), %% pong

 

d)  連接斷開

ok = gen_tcp:send(Socket, << 1:1, 0:3, 8:4, 0:8 >>), %% close

{ok, << 1:1, 0:3, 8:4, 0:8 >>} = gen_tcp:recv(Socket, 0, 6000),

 

 

 6、HTTP協議之rest-api

      未完待續


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM