天天看點

《ranch 源代碼分析 (Erlang實作的網絡庫)》

最新版本的cowboy已經把和網絡相關的邏輯放到了ranch的代碼裡。ranch是一個類似ANET的網絡庫. 它的設計目标是高并發,低延遲,子產品化。

之前一直在使用C++,最近學習了erlang,對比一下這兩門不同思路的語言

1) 語言底層:最顯着的不同是erlang是解釋型動态語言,C++是靜态的。
2) 語言功能:Erlang是函數式和輕量級程序(要徹底了解函數式最直接的就是通讀SICP)。變量不可變。是以,函數是沒有狀态的,狀态展現在函數的參數上。而C++可以修改一切記憶體.
   另外輕量級程序:程序是使用者态級别的結構體。是以程序的建立和切換代價很小。而C++的pthread對應系統的一個線程.
3) 設計範式:個人了解,從代碼上erlang中的一個子產品對應C++的一個類,都是封裝了對一個實體的操作. 不同的是C++的操作是和對象綁定的(編譯器自動把對象的指針作為成員函數的第一個參數),而erlang是手動把要操作的實體作為第一個參數。
   erlang中的程序對應到C++的一份對象; erlang中的一個消息對應c++的一次對象的函數調用; 由于erlang中的程序是廉價的,是以有着和C++完全不同的面向程序的程式設計範式。是以OTP抽象出了’server模式‘,’監督模式‘,’狀态機模式‘等模式。
   而C++的23種設計模式本身就是基于單線程的思路。
   舉個例子,在C++中調用成員函數 `A a; a.f();' 被編譯器翻譯成 `A::f(&a);'。
   而erlang中調用子產品的函數修改對象的狀态,要這樣做 `A:f(Ref)',Ref是要修改的對象,A:f是修改對象的方法。      

一個例子 hello,world!

ranch的源碼自帶了一個tcp_echo的例子

1) 要使用ranch首先要啟動ranch,application:start(ranch)。
2) 提供一個回調子產品echo_protocol。
3) 啟動ranch監聽ranch:start_ranch_acceptors_suplistener。      

深入代碼

從application:start(ranch)開始

ranch_sup監督者會啟動一個ranch_server,負責維護配置資訊,低層存儲用ets。ranch_server是gen_server模式,接收各種消息。
仔細察看ranch_server.erl。這個子產品提供了操作ranch_server的各種函數。
這個子產品檔案中所有被export出來的函數,就是對外部的接口,即外部子產品可以通過調用這些子產品改變’對象‘的狀态。對象就是ranch_server程序。對象的指針就是每個函數的第一個參數。
舉個例子 函數`set_port(Ref, Port)'修改Ref指向對象的port。      

使用者的代碼是從 ranch:start_listener開始

使用者需要指定網絡參數

{ok, _} = ranch:start_listener(tcp_echo, 1,
        ranch_tcp, [{port, 5555}], echo_protocol, []),      
注意:整個ranch對外面的接口全部在ranch.erl中。這裡通過調用ranch:start_listener啟動監聽,同時提供一個資料到達後的回調函數。
 tcp_echo:标志ranch之上的應用。所有的操作都要把tcp_echo帶上。
 ranch_tcp:ranch提供tcp和ssl兩種協定。實作多态.
 [{port, 5555}]:tcp的參數。
 echo_protocol:回調函數。      

ranch啟動服務

Res = supervisor:start_child(ranch_sup, child_spec(Ref, NbAcceptors,
         Transport, TransOpts, Protocol, ProtoOpts)),      
注意:此時代碼還是運作在tcp_echo的程序中。
 
 ranch:start_listener在ranch_sup的監督樹中添加一個ranch_listener_sup的監督程序。程序的描述由函數 `child_spec` 産生。      
{{ranch_listener_sup, Ref}, {ranch_listener_sup, start_link, [
        Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts
    ]}, permanent, 5000, supervisor, [ranch_listener_sup]}.      
到這裡tcp_echo_app傳回。下面的工作交給ranch内部的相關子產品。      

ranch_listener_sup 監督者

這個子產品是ranch中最重要的子產品了。acceptors 池的管理,conns的管理都是該子產品負責.      
《ranch 源代碼分析 (Erlang實作的網絡庫)》
ranch_listener_sup 會啟動兩個監督者:ranch_conns_sup 和 ranch_acceptors_sup。      
init({Ref, NbAcceptors, Transport, TransOpts, Protocol}) ->
    ChildSpecs = [
        %% conns_sup
        {ranch_conns_sup, {ranch_conns_sup, start_link,
                [Ref, Transport, Protocol]},
            permanent, infinity, supervisor, [ranch_conns_sup]},
        %% acceptors_sup
        {ranch_acceptors_sup, {ranch_acceptors_sup, start_link,
                [Ref, NbAcceptors, Transport, TransOpts]
            }, permanent, infinity, supervisor, [ranch_acceptors_sup]}
    ],
    {ok, {{rest_for_one, 10, 10}, ChildSpecs}}.      
注意:一定要把ranch_conns_sup放在前面。監督者行為模式會根據順序啟動.      

ranch_conns_sup監督者

起的是監督者的作用,監控處理連接配接的程序 (回調函數運作的程序)。但并沒有實作 supervisor 模式,因為這個監督者做的事情并不是簡單的啟動子程序。還要做檢查子程序的狀态,處理最大連接配接數等。      
start_link(Ref, Transport, Protocol) ->
    proc_lib:start_link(?MODULE, init, [self(), Ref, Transport, Protocol]).      
啟動一個子程序,執行init函數。init函數初始化loop的狀态。      
%% CurConns -> 目前連接配接數目
     %% MaxConns -> 最大連接配接數
     %% conns sup 程序一直循環使用 receive 接收消息
     loop(State=#state{parent=Parent, ref=Ref,
                transport=Transport, protocol=Protocol, opts=Opts,
        max_conns=MaxConns}, CurConns, NbChildren, Sleepers) ->
        receive
    %% ranch acceptor 擷取 client socket 後,發送消息給 conns sup 程序
    %% 此時 client socket 控制程序為 conns sup
        {?MODULE, start_protocol, To, Socket} ->
                     %% 啟動新程序,子程序就是最開始的地方使用者指定的回調 echo_protocol:start_link
            case Protocol:start_link(Ref, Socket, Transport, Opts) of 
                {ok, Pid} ->
                             %% 把socket的控制權從目前程序轉給echo_protocol子程序,隻有這樣它才能收到網絡資料。
                    %% 注意:這個時候子程序是阻塞住的,這是必須的!因為這個時候子程序還沒有控制權,收不到資料。
                    Transport:controlling_process(Socket, Pid),
                    %% 給子程序發送消息, 喚醒子程序。
                    Pid ! {shoot, Ref}, 
                    put(Pid, true),
                    CurConns2 = CurConns + 1,
                    %% 如果還沒有到達最大的連接配接數目
                    if CurConns2 < MaxConns ->
                                     %% 給 acceptor 程序發送消息,此時acceptor程序阻塞在start_protocol函數的receive
                            To ! self(),  
                            loop(State, CurConns2, NbChildren + 1,
                                Sleepers);
                        true ->
                        %% 如果大于最大連接配接數,目前 acceptor 進入休眠隊列(acceptor程序一直在等待消息)
                        %% 注意:Sleeper多了一個acceptor程序。Erlang就是通過不停的更改參數,來改變程序的狀态。
                            loop(State, CurConns2, NbChildren + 1,
                                [To|Sleepers])
                    end;
            end;
        %% echo_protocol子程序exit時,發送 EXIT 新号給連接配接的程序,也就是 conns sup
        %% 通過設定trap_exit,來捕獲退出消息。
        %% 從Sleeper的頭部取出一個acceptor,給它發送消息,讓它喚醒,繼續接受新的連接配接。
        {'EXIT', Pid, _} ->
            erase(Pid),
            [To|Sleepers2] = Sleepers,
            To ! self(),
            loop(State, CurConns - 1, NbChildren - 1, Sleepers2);
    end.      
一個簡化版的loop函數。列出了兩個最重要的消息類型。
 1) start_protocol消息。這個消息的發送由調用本子產品的start_protocol函數産生。在ranch_acceptor子產品中,當有一個新的連接配接進來後調用該函數.      
start_protocol(SupPid, Socket) ->
            SupPid ! {?MODULE, start_protocol, self(), Socket},
            %% 這個地方ranch_acceptor會同步的等在這個地方 !!!
            receive SupPid -> ok end.      
這個消息會使ranch_conns_sup啟動一個子程序 ‘Protocol:start_link(Ref, Socket, Transport, Opts)’。Protocol就是使用者最初提供的一個回調。
 2) ‘EXIT’消息, 通過設定 process_flag(trap_exit, true) 可以捕獲到子程序的退出消息。并且喚醒一個阻塞的acceptor程序。      

ranch_acceptors_sup監督者

執行accept程序的監督者,accept程序的個數是由NbAcceptors控制。
首先使用子產品Transport:listen監聽端口。
注意:Transport是一個變量,也就是說Erlang可以通過這種方式實作多态。
然後把conns_sup監督者的pid和listen傳回的socket傳給每一個acceptor子程序。      
%% 參數:(tcp_echo, 1, ranch_tcp, [{port, 5555}])
    init([Ref, NbAcceptors, Transport, TransOpts]) ->
    {ok, Socket} = Transport:listen(TransOpts),
    %% acceptor 池配置的多大,就有多少 ranch_acceptor
    Procs = [
        {{acceptor, self(), N}, {ranch_acceptor, start_link, [
            LSocket, Transport, ConnsSup
        ]}, permanent, brutal_kill, worker, []}
            || N <- lists:seq(1, NbAcceptors)],
    {ok, {{one_for_one, 10, 10}, Procs}}.
    
    ```
#### ranch_acceptor程序
     ranch_acceptor的個數和Nbacceptors一樣。
     Erlang中可以多個程序同時accept一個socket。
     ```
     loop(LSocket, Transport, ConnsSup) ->
    _ = case Transport:accept(LSocket, infinity) of
        {ok, CSocket} ->
                %% 擷取的連接配接的socket控制權轉交給conns_sup程序,conns_sup再把控制權轉交給它的子程序。
            Transport:controlling_process(CSocket, ConnsSup),
            %% 同步的調用,需要等待 conns sup 程序 生成echo_protocol子程序後,傳回 self() 為止。
            ranch_conns_sup:start_protocol(ConnsSup, CSocket);
            
        {error, Reason} when Reason =/= closed ->
            ok
    end,
    ?MODULE:loop(LSocket, Transport, ConnsSup)      

比較一下ranch和anet

1) ranch和anet都是高并發的網絡庫; ranch可以處理海量連結, anet可以處理高吞吐量, 低延遲;

2) 使用的時候都要提供一個回調函數;

3) anet封裝了packet, 而ranch提供的還是流服務. 這是應用場景決定的: anet面向的内網, 内網一般是基于特定的業務, 而業務的資料是有結構的. ranch既可以面向内網,也可以面向外網. 而有的應用場景本身就是流式的, 比如http協定. ranch可以不需要等待一個http請求頭部到達後再處理, 可以一邊接收http請求頭一遍解析http的狀态機. 當然, 可以等到全部的http請求頭到達後在一次性解析. 但是這樣處理對于Erlang來說效率是慢的. 對于anet來說效率是快的. 使用者可以在回調中實作自己的協定, 使得ranch支援有結構的資料包.

4) anet中的逾時是使用了一個timeout 線程, 每隔一段時間掃面一遍所有的連結最後一次收到資料的時間. 而ranch的逾時可以在回調中receive設定逾時,很自然的支援.

5) anet支援同一個連結複用不用的請求,通過channelID實作. client端每次發送包的時候都要動态生成一個包的channelID, 并且把<channelID, handler>插入到hashtable中. 發送給server的資料標頭部帶上這個channeldID. server端傳回的時候把channelID 原值傳回給client端. client端通過channelID 找到對應的handler. 而Erlang中處理并發的思路就是直接一個程序. 一個請求對應一個程序.

繼續閱讀