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

      未完待续

posted @ 2013-01-21 19:40  小壁虎借尾巴  阅读(3334)  评论(3编辑  收藏  举报