1. 程式人生 > 實用技巧 >第十六章 OTP概述

第十六章 OTP概述

16.1 通用伺服器程式的進化路線

16.1.1 server1: 原始伺服器程式

服務端實現

-module(server1).
-export([start/2, rpc/2]).

%% 啟動服務
start(Name, Mod) ->
    %% 註冊程序名為Name, 並在啟動程序時完成模組Mod的初始化並在呼叫loop進行監測
    register(Name, spawn(fun() ->loop(Name, Mod, Mod:init()) end)).

%% 遠端呼叫
rpc(Name, Request) ->
    %% 向指定的程序傳送請求, 使用self()獲得自身的Pid, 用於識別請求者
    Name ! {self(), Request},
    receive
        %% 接收到處理結果後輸出
        {Name, Response} ->Response
    end.

loop(Name, Mod, State) ->
    receive
        %% 接收到請求後的處理
        {From, Request} ->
            %% 呼叫Mod的handle函式進行處理
            {Response, State1} = Mod:handle(Request, State),
            %% 將處理結果傳送給請求者
            From ! {Name, Response},
            loop(Name, Mod, State1)
    end.

回撥程式實現

-module(name_server).
-import(server1, [rpc/2]).
-export([init/0, add/2, whereis/1, handle/2]).

%% 對外提供的功能函式, 會通過遠端呼叫向指定的服務名傳送請求
add(Name, Place) ->rpc(name_server, {add, Name, Place}).
whereis(Name)    ->rpc(name_server, {whereis, Name}).

%% 模組初始化, 建立新的字典
init() ->dict:new().

%% 處理函式, 新增和查詢
handle({add, Name, Place}, Dict) ->{ok, dict:store(Name, Place, Dict)};
handle({whereis, Name}, Dict)    ->{dict:find(Name, Dict), Dict}. 

執行結果

1> c(server1).
{ok,server1}
2> c(name_server).
{ok,name_server}
3> server1:start(name_server, name_server).
true
4> name_server:add(joe, "at home").
ok
5> name_server:whereis(joe).
{ok,"at home"}

處理流程:

1. server1:start(name_server, name_server).
啟動程序, 完成name_server模組的初始化, 指定使用name_server的handle函式處理請求, 最後將程序註冊為name_server

2. name_server:add(joe, "at home").
功能函式相當於是一個介面, 將資料封裝為{add, joe, "at home"}格式, 呼叫rpc提交請求

3. rpc(Name, Request)
 Name ! {self(), Request}
獲取自身程序ID後將資料傳送給name_server程序

4. loop(Name, Mod, State)
{Response, State1} = Mod:handle(Request, State)
接收到請求後呼叫name_server:handle處理請求

5. handle({add, Name, Place}, Dict)
回撥函式, 相當於介面的具體實現, 向字典中新增資料

6. loop(Name, Mod, State) 
From ! {Name, Response}
將處理結果傳送給請求者

7. rpc(Name, Request)
{Name, Response} -> Response 
列印處理結果

16.1.2 server2: 支援事務的伺服器程式

服務端實現

-module(server2).
-export([start/2, rpc/2]).

%% 啟動服務
start(Name, Mod) ->
    %% 註冊程序名為Name, 並在啟動程序時完成模組Mod的初始化並在呼叫loop進行監測
    register(Name, spawn(fun() ->loop(Name, Mod, Mod:init()) end)).

%% 遠端呼叫
rpc(Name, Request) ->
    %% 向指定的程序傳送請求, 使用self()獲得自身的Pid, 用於識別請求者
    Name ! {self(), Request},
    receive
        %% 添加了出錯處理
        {Name, crash}        ->exit(rpc);
        %% 接收到處理結果後輸出
        {Name, ok, Response} ->Response
    end.

loop(Name, Mod, OldState) ->
    receive
        %% 接收到請求後的處理
        {From, Request} ->
            %% 對Mod的handle函式呼叫新增異常處理
            try Mod:handle(Request, OldState) of
                {Response, NewState} ->
                    From ! {Name, ok, Response},
                    loop(Name, Mod, NewState)
            catch
                _:Why ->
                    %% 發生異常則列印異常資訊
                    log_the_error(Name, Request, Why),
                    From ! {Name, crash},
                    %% 保留舊的狀態
                    loop(Name, Mod, OldState)
            end
    end.

log_the_error(Name, Request, Why) ->
    io:format("Server ~p request ~p ~n caused exception ~p~n", [Name, Request, Why]).

16.1.3 server3: 支援熱程式碼替換的伺服器程式

在服務端添加了替換程式碼的函式

swap_code(Name, Mod) ->rpc(Name, {swap_code, Mod}).

loop(Name, Mod, OldState) ->
    receive
        {From, {swap_code, NewCallBackMod}} ->
            From ! {Name, ack},
            %% 改變loop迴圈用於處理請求的模組
            loop(Name, NewCallBackMod, OldState);
        {From, Request} ->
            {Response, NewState} = Mod:handle(Request, OldState),
            From ! {Name, Response},
            loop(Name, Mod, NewState)
    end.

執行結果:

1> server3:start(name_server, name_server1).
true
2> name_server1:add(joe, "at home").
ok
3> name_server1:add(helen, "at work").
ok
4> c(new_name_server).
{ok,new_name_server}
5> server3:swap_code(name_server, new_name_server).
ack
6> new_name_server:all_names().
[joe,helen]
7> new_name_server:delete(joe).
ok
8> new_name_server:all_names().
[helen]
9> new_name_server:whereis(helen).
{ok,"at work"}

16.1.4 server4: 同時支援事務和熱程式碼替換

顯然, 將server2中的異常捕獲處理新增到server3中即可。

16.1.5 server5: 壓軸好戲

空伺服器的實現

-module(server5).
-export([start/0, rpc/2]).

%% 啟動程序
start() ->spawn(fun() ->wait() end).

%% 等待接收指令成為某種服務
wait() ->
    receive
        {become, F} ->F()
    end.

%% 遠端呼叫
rpc(Pid, Q) ->
    %% 由服務程序處理請求
    Pid ! {self(), Q},
    receive
        %% 列印處理結果
        {Pid, Reply} ->Reply
    end.

具體服務的一個實現

-module(my_fac_server).
-export([loop/0]).

%% 階乘服務
loop() ->
    receive
        %% 可以接收數字計算其階乘
        {From, {fac, N}} ->
            From ! {self(), fac(N)},
            loop();
        %% 也可以變成另外一種服務
        {become, Something} ->Something()
    end.

fac(0) ->1;
fac(N) ->N * fac(N-1). 

執行結果:

1> c(server5).
{ok,server5}
2> Pid = server5:start().
<0.52.0>
3> c(my_fac_server).
{ok,my_fac_server}
4> Pid ! {become, fun my_fac_server:loop/0}.
{become,#Fun<my_fac_server.loop.0>}
5> server5:rpc(Pid, {fac, 30}).
265252859812191058636308480000000

執行流程:

1. 啟動程序, 等待接收指令成為某種服務
Pid = server5:start().
start() -> spawn(fun() -> wait() end).

2. 接收指令, 成為可以計算階乘的服務
Pid ! {become, fun my_fac_server:loop/0}. 

3. 服務呼叫
server5:rpc(Pid, {fac, 30}).

4. rpc(Pid, Q)
%% 由服務程序處理請求
Pid ! {self(), Q} 

5. 處理請求
loop() ->
    receive
        {From, {fac, N}} ->
            From ! {self(), fac(N)}
            ...
6. 接收結果並輸出
rpc(Pid, Q)
    receive
        {Pid, Reply} -> Reply
    end.

16.2 gen_server起步

16.2.1 第一步: 確定回撥模組的名稱

模擬支付系統, 模組命名為my_bank

16.2.2 第二步: 寫介面函式

%% 啟動本地伺服器
%% gen_server:start_link({local, Name}, Mod, _)
start() ->gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).

%% 遠端呼叫
%% gen_server:call(Name, Term)
stop()  ->gen_server:call(?MODULE, stop).

%% 開一個新賬戶
new_account(Who)      ->gen_server:call(?MODULE, {new, Who}).
%% 存錢
deposit(Who, Amount)  ->gen_server:call(?MODULE, {add, Who, Amount}).
%% 取錢
withdraw(Who, Amount) ->gen_server:call(?MODULE, {remove, Who, AMount}).

16.2.3 第三步: 編寫回調函式

%% init([]) -> {ok, State}
%% 必須實現的回撥函式, 用於模組初始化
%% 這裡返回一個ETS表
init([]) ->{ok, ets:new(?MODULE, [])}.

%% handle_call(_Request, _From, State) -> {reply, Reply, State}
%% 必須實現的回撥函式, 用於gen_server:call時回撥使用

%% 新增使用者
handle_call({new, Who}, _From, Tab) ->
    %% 查詢ETS表中相關使用者是否存在
    %% 不存在則插入ETS表並提示歡迎資訊
    %% 存在則提示已經存在此使用者
    Reply = case ets:lookup(Tab, Who) of
                []  ->ets:insert(Tab, {Who, 0}),
                      {welcome, Who};
                [_] ->{Who, you_already_are_a_customer}
            end,
    {reply, Reply, Tab};

%% 使用者存錢
handle_call({add, Who, X}, _From, Tab) ->
    %% 首先查詢使用者是否存在
    %% 存在則將存款累加後重新插入ETS表並給出提示資訊
    Reply = case ets:lookup(Tab, Who) of
                [] ->not_a_customer;
                [{Who, Balance}] ->
                    NewBalance = Balance + X,
                    ets:insert(Tab, {Who, NewBalance}),
                    {thanks, Who, your_balance_is, NewBalance}
            end,
    {reply, Reply, Tab};

%% 使用者取錢
handle_call({remove, Who, X}, _From, Tab) ->
    %% 首先查詢使用者是否存在
    %% 存在則根據取款與存款的大小關係分別處理
    Reply = case ets:lookup(Tab, Who) of
                [] ->not_a_customer;
                [{Who, Balance}] when X =< Balance ->
                    NewBalance = Balance - X,
                    ets:insert(Tab, {Who, NewBalance}),
                    {thanks, Who, your_balance_is, NewBalance};
                [{Who, Balance}] ->
                    {sorry, Who, you_only_have, Balance, in_the_bank}
            end,
    {reply, Reply, Tab};

%% 停止服務
handle_call(stop, _From, Tab) ->
    {stop, normal, stopped, Tab}.

%% 其它必須實現的回撥函式
handle_cast(_Msg, State) ->{noreply, State}.
handle_info(_Info, State) ->{noreply, State}.
terminate(_Reason, _State) ->ok.
code_change(_OldVsn, State, Extra) ->{ok, State}.

執行結果:

1> my_bank:start().
{ok,<0.70.0>}
2> my_bank:deposit("joe", 10).
not_a_customer
3> my_bank:new_account("joe").
{welcome,"joe"}
4> my_bank:deposit("joe", 10).
{thanks,"joe",your_balance_is,10}
5> my_bank:deposit("joe", 30).
{thanks,"joe",your_balance_is,40}
6> my_bank:withdraw("joe", 15).
{thanks,"joe",your_balance_is,25}
7> my_bank:withdraw("joe", 45).
{sorry,"joe",you_only_have,25,in_the_bank}
8> my_bank:stop().             
stopped

16.3 gen_server回撥的結構

16.3.1 啟動伺服器程式時發生了什麼

%% 通過start_link啟動
%% 建立名為Name的通用伺服器程式
%% 回撥模組為Mod
%% Opts控制伺服器程式的行為
%% 呼叫Mod:init(InitArgs)啟動伺服器程式 
gen_server:start_link(Name, Mod, InitArgs, Opts)

16.3.2 呼叫伺服器程式時發生了什麼

%% 通過call呼叫服務端程式
%% 最終會呼叫回撥模組中的handle_call/3函式 
gen_server:call(Name, Request)

%% Request 請求資訊
%% From    發起呼叫的客戶端程序ID
%% State   客戶端當前狀態
Mod:handle_call(Request, From, State)

16.3.3 呼叫和通知

%% 通過cast實現通知
%% 最終會呼叫回撥模組中的handle_cast/2函式 
gen_server:cast(Name, Name)

%% Msg   傳送的訊息
%% State 狀態 
Mod:handle_cast(Msg, State)

16.3.4 傳送給伺服器的原生訊息

%% 接收其它程序或系統傳送的訊息 
Mod:handle_info(Info, State)

16.3.5 Hasta La Vista, Baby

%% 這個標題, Joe真有點意思:)
Mod:terminate(Reason, NewState)

16.3.6 熱程式碼替換

code_change(OldVsn, State, Extra)