Erlang——OTP
OTP,应用程序操作系统,用了构造容错系统。
1.服务器进化历程
原始服务器程序
服务器的基本模式(通过回调模块定制):
-module(server1).
%%最初的服务器程序
-author("").
%%%=======================EXPORT=======================
-export([start/2, rpc/2]).
start(Name, Mod) ->
register(Name, spawn(fun() ->loop(Name, Mod, Mod:init()) end)).
rpc(Name, Request) ->
Name ! {self(), Request},
receive
{Name, Response} ->Response
end.
loop(Name, Mod, State) ->
receive
{From, Request} ->
{Response, State1} = Mod:handle(Request, State),
From ! {Name, Response},
loop(Name, Mod, State1)
end.
server1的回调函数:
-module(name_server).
-author("").
%%%=======================EXPORT=======================
-import(server1, [rpc/2]).
%%server1的回调程序
-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> server1:start(name_server,name_server).
true
2> name_server:add(harry,"at home").
ok
3> name_server:whereis(harry).
{ok,"at home"}
支持事务的服务器程序
代码演示:
-module(server2).
-author("").
%%%=======================EXPORT=======================
-export([start/2, rpc/2]).
start(Name, Mod) ->
register(Name, spawn(fun() -> loop(Name, Mod, Mod:init()) end)).
rpc(Name, Request) ->
Name ! {self(), Request},
receive
{Name, crash} -> exit(rpc);
{Name, ok, Response} -> Response
end.
loop(Name, Mod, OldState) ->
receive
{From, Request} ->
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]).
在请求导致服务器程序出现异常时,会让客户端代码异常退出。
这个版本的服务器提供了一种事务机制,如果在handler函数中发生了异常,他会用之前的状态进行循环,只有当handler函数正常返回时,才会用handler函数返回的新状态进行循环。之所以要保留前一个状态,是当handler函数失效时,发起调用的客户端会收到一个让他立刻退出的消息。因为请求导致了handler函数失败,所以客户端的调用是无法成功的,但受到影响的也只有这一个客户端而已,连到同一服务器上,其他客户端不会受到任何影响。另外,当handler函数中发生错误时,服务器的状态也没有因此而发生任何改变。
相较上个版本回调模块,没有任何改动(只需要改下导入的模块名,改成了server2即可),只是改变了服务器代码本身,我们可以独立的对非功能的行为部分进行更改,功能性的部分不受影响。
支持热代码替换的服务器程序
-module(server3).
-author("").
-export([start/2, rpc/2, swap_code/2]).
start(Name, Mod) ->
register(Name, spawn(fun() -> loop(Name, Mod, Mod:init()) end)).
swap_code(Name, Mod) -> rpc(Name, {swap_code, Mod}).
rpc(Name, Request) ->
Name ! {self(), Request},
receive
{Name, Response} -> Response
end.
loop(Name, Mod, OldState) ->
receive
{From, {swap_code, NewCallBackMod}} ->
From ! {Name, ack},
loop(Name, NewCallBackMod, OldState);
{From, Request} ->
{Response, NewState} = Mod:handle(Request, OldState),
From ! {Name, Response},
loop(Name, Mod, NewState)
end.
他硬编码了服务器的名字,做一个新的name_server1
-module(name_server1).
-author("").
-import(server3, [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}.
用name_server1启动server3:
1> server3:start(name_server,name_server1).
true
2> name_server:add(harry,"at home").
ok
3> name_server:add(moka,"at work").
ok
可成功存入
同时支持事务和热代码替换的服务器程序
-module(server4).
-author("").
%%%=======================EXPORT=======================
-export([start/2, rpc/2, swap_code/2]).
start(Name, Mod) ->
register(Name, spawn(fun() -> loop(Name, Mod, Mod:init()) end)).
swap_code(Name, Mod) -> rpc(Name, {swap_code, Mod}).
rpc(Name, Request) ->
Name ! {self(), Request},
receive
{Name, crash} -> exit(rpc);
{Name, Response} -> Response
end.
loop(Name, Mod, OldState) ->
receive
{From, {swap_code, NewCallBackMod}} ->
From ! {Name, ok, ack},
loop(Name, NewCallBackMod, OldState);
{From, Request} ->
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]).
可以同时支持事务机制和热代码替换
支持热代码替换的服务器程序
不提供服务,直至接收到指令:
-module(server5).
-author("").
-export([start/0, rpc/2]).
start() ->
spawn(fun() -> wait() end).
rpc(Pid, Q) ->
Pid ! {self(), Q},
receive
{Pid, Reply} -> Reply
end.
wait() ->
receive
{become, F} ->
F()
end.
定义服务器函数:
-module(my_fac_server).
-author("").
%%%=======================EXPORT=======================
-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(my_fac_server).
{ok,my_fac_server}
2> Pid=server5:start().
<0.84.0>
3> Pid!{become,fun my_fac_server:loop/0}.
{become,fun my_fac_server:loop/0}
4> server5:rpc(Pid,{fac,30}).
265252859812191058636308480000000
2.gen_server
模块
以一个简单的支付系统为例(my_bank)
-module(my_bank).
-author("wangjiaqi").
-export([start/0,stop/0,new_account/1,deposit/2,withdraw/2]).
start()-> % 打开银行
gen_server:start_link({local,?MODULE},?MODULE,[],[]).
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}).
?MODULE宏展开时会对应模块名称即my_bank
gen_server:start_link启动一个本地服务器
gen_server:call函数用于发起对服务器的远程调用
回调函数
-export([start_link/0,init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2,code_change/3]).
start_link()->gen_server:start_link({local,?MODULE},?MODULE,[],[]).
init([])->{ok,ets:new(?MODULE,[])}.
handle_call({new,Who},_From,Tab)->
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)->
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,your_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(_Request,_State)->ok.
code_change(_OldVsn,State,Extra)->{ok,State}.
通过gen_server:start_link调用启动服务器程序,回调模块的第一个被调用的函数是init,他返回{ok,State}.State值会作为handle_call函数的第三个参数再次出现.
handle_call(stop,_From,Tab)终止服务器程序,返回{stop,normal,stopped,Tab},是终止服务器的方法。normal作为terminate的参数,stopped会成为my_rank:stop()的返回值。
整合代码:
-module(my_bank).
%%%=======================EXPORT=======================
-export([start/0,stop/0,new_account/1,deposit/2,withdraw/2]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
%%%=======================INCLUDE======================
%%%=======================RECORD=======================
%%%=======================DEFINE=======================
%%%=======================TYPE=========================
%%%=================EXPORTED FUNCTIONS=================
%% ----------------------------------------------------
%% Description:
%% ----------------------------------------------------
start()-> % 打开银行
gen_server:start_link({local,?MODULE},?MODULE,[],[]).
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}).
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
init([])->{ok,ets:new(?MODULE,[])}.
handle_call({new,Who},_From,Tab)->
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)->
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,your_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> c(my_bank).
my_bank.erl:34: Warning: function start_link/0 is unused
my_bank.erl:81: Warning: variable 'Extra' is unused
{ok,my_bank}
2> my_bank:start().
{ok,<0.84.0>}
3> my_bank:deposit("harry",100).
not_a_customer
4> my_bank:new_account("harry").
{welcome,"harry"}
5> my_bank:deposit("harry",100).
{thanks,"harry",your_balance_is,100}
6> my_bank:deposit("harry",121).
{thanks,"harry",your_balance_is,221}
7> my_bank:withdraw("harry",10).
{thanks,"harry",your_balance_is,211}
8> my_bank:withdraw("harry",300).
{sorry,"harry",your_only_have,211,in_the_bank}
输入为非数字型会报错,所有数据丢失
改进(加输入容错):
-module(my_bank).
%% API
-export([start/0, stop/0, new_account/1, deposit/2, withdraw/2]).
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(DATA_FILE, "bank_data.txt").
-define(DATA_PATH, "./").
start() -> gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
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}).
init([]) ->
case filelib:find_file(?DATA_FILE, ?DATA_PATH) of
{ok, _} -> %% 文件存在,加载数据
ets:file2tab(?DATA_PATH ++ ?DATA_FILE); %% {ok, Tab}
{error, not_found} -> %% 文件不存在,新建文件
{ok, ets:new(?MODULE, [named_table])}
end.
handle_call({new, Who}, _From, Tab) ->
Reply = case ets:lookup(Tab, Who) of
[] ->
ets:insert(Tab, {Who, 0}),
{welcome};
[_] ->
{you_already_are_a_customer}
end,
{reply, Reply, Tab};
handle_call({add, Who, Amount}, _From, Tab) when Amount > 0 ->
Reply = case ets:lookup(Tab, Who) of
[] ->
{not_a_customer};
[{Who, Balance}] ->
NewBalance = Balance + Amount,
ets:insert(Tab, {Who, NewBalance}),
{success, now_balance_is, NewBalance}
end,
{reply, Reply, Tab};
handle_call({add, _, _}, _, Tab) ->
{reply, {error}, Tab};
handle_call({remove, Who, Amount}, _From, Tab) when Amount > 0 ->
Reply = case ets:lookup(Tab, Who) of
[] ->
{not_a_customer};
[{Who,Balance}] when Amount =< Balance ->
NewBalance = Balance - Amount,
ets:insert(Tab, {Who, NewBalance}),
{success, now_balance_is, NewBalance};
[{Who,Balance}] ->
{error, you_only_have, Balance}
end,
{reply, Reply, Tab};
handle_call({remove, _, _}, _, Tab) ->
{reply, {error}, 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) ->
Result = ets:tab2file(?MODULE, ?DATA_PATH ++ ?DATA_FILE),
io:format("~p~n", [Result]),
ok.
code_change(_OldVsn, State, _Extra) -> {ok, State}.
执行结果:
1> c(my_bank).
{ok,my_bank}
2> my_bank:start().
{ok,<0.84.0>}
3> my_bank:new_account(harry).
{welcome}
4> my_bank:deposit(harry,100).
{success,now_balance_is,100}
5> my_bank:withdraw(harry,25).
{success,now_balance_is,75}
6> my_bank:deposit(harry,x).
ok
=ERROR REPORT==== 18-Sep-2023::23:33:15.983000 ===
** Generic server my_bank terminating
** Last message in was {add,harry,x}
** When Server state == my_bank
** Reason for termination ==
** {badarith,[{erlang,'+',[75,x],[]},
{my_bank,handle_call,3,[{file,"my_bank.erl"},{line,200}]},
{gen_server,try_handle_call,4,
[{file,"gen_server.erl"},{line,661}]},
{gen_server,handle_msg,6,[{file,"gen_server.erl"},{line,690}]},
{proc_lib,init_p_do_apply,3,
[{file,"proc_lib.erl"},{line,249}]}]}
** Client <0.77.0> stacktrace
** [{gen,do_call,4,[{file,"gen.erl"},{line,167}]},
{gen_server,call,2,[{file,"gen_server.erl"},{line,211}]},
{erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,684}]},
{shell,exprs,7,[{file,"shell.erl"},{line,686}]},
{shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
{shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]
=CRASH REPORT==== 18-Sep-2023::23:33:15.983000 ===
crasher:
initial call: my_bank:init/1
pid: <0.84.0>
registered_name: my_bank
exception error: an error occurred when evaluating an arithmetic expression
in operator +/2
called as 75 + x
in call from my_bank:handle_call/3 (my_bank.erl, line 200)
in call from gen_server:try_handle_call/4 (gen_server.erl, line 661)
in call from gen_server:handle_msg/6 (gen_server.erl, line 690)
ancestors: [<0.77.0>]
message_queue_len: 0
messages: []
links: [<0.77.0>]
dictionary: []
trap_exit: false
status: running
heap_size: 2586
stack_size: 27
reductions: 10198
neighbours:
neighbour:
pid: <0.77.0>
registered_name: []
initial_call: {erlang,apply,2}
current_function: {gen,do_call,4}
ancestors: []
message_queue_len: 0
links: [<0.64.0>,<0.84.0>]
trap_exit: false
status: waiting
heap_size: 987
stack_size: 33
reductions: 7427
current_stacktrace: [{gen,do_call,4,[{file,"gen.erl"},{line,167}]},
{gen_server,call,2,[{file,"gen_server.erl"},{line,211}]},
{erl_eval,do_apply,6,[{file,"erl_eval.erl"},{line,684}]},
{shell,exprs,7,[{file,"shell.erl"},{line,686}]},
{shell,eval_exprs,7,[{file,"shell.erl"},{line,642}]},
{shell,eval_loop,3,[{file,"shell.erl"},{line,627}]}]
** exception exit: badarith
in operator +/2
called as 75 + x
in call from my_bank:handle_call/3 (my_bank.erl, line 200)
in call from gen_server:try_handle_call/4 (gen_server.erl, line 661)
in call from gen_server:handle_msg/6 (gen_server.erl, line 690)
in call from proc_lib:init_p_do_apply/3 (proc_lib.erl, line 249)
7> my_bank:start().
{ok,<0.95.0>}
8> my_bank:deposit(harry,100).
{success,now_balance_is,175}
可见尽管输入字母会报错,但重新开启后数据仍存在。