虚拟银行 ErlyBank 与 gen_server
本文是介绍Erlang/OTP系列文章的第一篇。
场景:银行ErlyBank的服务器开始运行,银行要求它成为可伸缩的系统,以便管理重要的顾客帐户。听说了Elang的强大功能后,银行雇用我们建造这一系统。为了测验我们的能力,银行首先要求我们建个简单的服务器,处理银行帐户开立和注销、存款、取款这几项业务。他们只要软件原型,无须满足实际需求。
结果:我们将使用 gen_server,搭建简单的服务器和客户端,实现银行的要求。因为只是做原型,我们就把帐户资料保存在内存里;帐户只包括用户名一项,不含其他资料。当然,存款、取款时,有确认的手续。
注意:开篇之前,假定你有Erlang语法基本知识。否则,建议你去学学。
什么是 gen_server ?
gen_server 是OTP的一项行为机制,是实现“客户/服务”关系的程序模块。它拥有许多东西,给你自由使用,这点以后再讲。以后,讲到监测器和运行时错误报告时,也会同样用到这个模块。
gen_server 的行为动作包括:
● init/1 - 服务器的初始化;
● handle_call/3 - 处理对服务器的同步调用。调用服务器的客户端被阻塞,直到本函数返回。
● handle_cast/2 - 处理对服务器的异步调用。调用的执行过程中,客户端不被阻塞。
● handle_info/2 - 是起着“收容”作用的函数。服务器收到的信息,如果不是同步调用或异步调用,就交由这个函数处理。例如,如果你的服务器与其他进程相连接,那么,要求退出进程的信息,就是这个函数处理。
● terminate/2 - 关闭服务器时,调用这个函数,做些善后处理。
● code_change/3 - 服务器运行中更新时,调用这个函数。在后面的文章中,会涉及这个函数的大量细节,但你应该至少会按照基本要求使用它。
服务器的基本结构
开写有关 gen_server的文件时,我总是使用通常的结构。你可以在文字编辑器中粘贴这个基本结构:
%%%-------------------------------------------------------------------
%%% File : eb_server.erl
%%% Author : Mitchell Hashimoto <mitchell.hashimoto@gmail.com>
%%% Description : The ErlyBank account server.
%%%
%%% Created : 5 Sep 2008 by Mitchell Hashimoto <mitchell.hashimoto@gmail.com>
%%%-------------------------------------------------------------------
-module(eb_server).
-behaviour(gen_server).
%% API
-export([start_link/0]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-record(state, {}).
-define(SERVER, ?MODULE).
%%====================================================================
%% API
%%====================================================================
%%--------------------------------------------------------------------
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
%% Description: Starts the server
%%--------------------------------------------------------------------
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%%====================================================================
%% gen_server callbacks
%%====================================================================
%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% Description: Initiates the server
%%--------------------------------------------------------------------
init([]) ->
{ok, #state{}}.
%%--------------------------------------------------------------------
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} |
%% {stop, Reason, State}
%% Description: Handling call messages
%%--------------------------------------------------------------------
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
%%--------------------------------------------------------------------
%% Function: handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% Description: Handling cast messages
%%--------------------------------------------------------------------
handle_cast(_Msg, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% Function: handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% Function: terminate(Reason, State) -> void()
%% Description: This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
ok.
%%--------------------------------------------------------------------
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
%% Description: Convert process state when code is changed
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
如你所见,模块名字是eb_derver,意思是“ErlyBank Server”。前面提到的服务器的响应函数,它全部都实现了,并且新增加了一个:
start_link/0。这个函数用于启动服务器:
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
它调用gen_server的函数start_link/4,启动服务器,向以宏SERVER代表的服务器注册进程。该宏的默认值是本模块名称。其他参数是gen_server模块本身,还有些选择项。因为不必另有选择,它们是空值。gen_server的更多解释,见相关手册。
初始化ErlyBank服务器
调用函数gen_server:start_link之后,它会调用实际服务器的函数init。对实际服务器的初始化状态设置,应该集中在函数init。可以任意设置状态的形式,如原子、函数等等。服务器做出响应时,会返回这些状态。因此,我们要维护一个列表,其中内容是全部帐户及其资金余额。另外,我们要以用户姓名查找这些帐户。为此,我打算使用dict模块,在内存中存储“键-值”数据对。
注意面向对象编程思想:你可以认为服务器状态是其实例化的变量。在每个gen_server的返回对象中,你会得到有效的实例变量,也可以修改它们,等等。
这是我最终的init函数:
init(_Args) ->
{ok, dict:new()}.
它的确很简单!它和ErlyBank的预期返回值之一都是 {ok, State}, 但我只返回ok和空字典来表示服务器状态。我们并不使用init的参数,它是来自start_link的空列表,因而它有个下划线前缀。
同步调用还是异步调用的问题
在我们实现服务器主体之前,我要再次重申同步调用与异步调用的区别。
对服务器的同步调用,会阻塞客户端。这就意味着,客户端向服务器发送消息时,它要一直等待答复。你若需要答复,例如询问某帐户的资金余额是多少,就要用同步调用。
对gen_server的异步调用,属非阻塞或异步的操作方法。也就是说,客户端向服务器发送消息后,不管是否收到答复,它都继续运行。
Erlang保证向进程发送的一切消息,都会收到,所以,除非你明确表示需要服务器的答复,你应该使用异步调用。换句话说,如果你只是简单地要服务器确认收到了消息,你不必发送同步调用给它,因为Erlang保证它会收到消息。(译注:作者米歇尔这里的理解可能错了,应该是不保证收到消息)
开立银行帐户
开立新帐户,是ErlyBank必须做的第一件事。快试试:如果你编写开设银行帐户的程序,那么,你调用服务器是同步还是异步?使劲想想,需要返回什么值?你若回答是同步调用,你对了。通常你要确信帐户已经开立,而非仅仅假定如此。不过,这里我的实现是要使用异步调用,因为现在不检查错误。
首先,我要写API函数,调用模块外的函数创建帐户:
%% --------------------------------------------------------------------
%% Function: create_account(Name) -> ok
%% Description: Creates a bank account for the person with name Name
%% --------------------------------------------------------------------
create_account(Name) ->
gen_server:cast(?SERVER, {create, Name}).
向服务器发送异步调用。服务器是我们在函数start_link中注册的,并以宏?SERVER代表的。发送的请求是个元组{create, Name}。由于是异步调用,会立刻给出“ok”提示,这也是被调用函数要返回的值。
现在,我们需要编写处理异步调用的函数,即处理回复gen_server的值:
handle_cast({create, Name}, State) ->
{noreply, dict:store(Name, 0, State)};
handle_cast(_Msg, State) ->
{noreply, State}.
如你所见,我们增加了函数handle_cast,获取开户请求,接着把它存入字典,数值0表示当前帐户资金余额。函数handle_cast预设返回的值是 {noreply, State} ,其中State是服务器的新状态。这次我们返回的是增加了新帐户的新字典。
还要注意,我加了一个“收容”函数handle_info。虽然这不是个好习惯,但FP编程一般都使这一招法。你可以用这个函数,不动声色地接收消息;否则,程序运行时的意外事件可能总是纠缠着你。
存款业务
我们曾向雇主银行ErlyBank承诺,要编写API处理存款进入帐户的业务,并且还要有基本的确认手续;在存款入帐前,服务器也要核验帐户是否存在。银行不愿让顾客的钱流入黑洞。再次考考自己,同步调用还是异步调用?显然是同步调用。我们必须确信存款的操作成功,并且告知顾客。
照旧,我首先写出API函数:
%% --------------------------------------------------------------------
%% Function: deposit(Name, Amount) -> {ok, Balance} | {error, Reason}
%% Description: Deposits Amount into Name's account. Returns the
%% balance if successful, otherwise returns an error and reason.
%% --------------------------------------------------------------------
deposit(Name, Amount) ->
gen_server:call(?SERVER, {deposit, Name, Amount}).
没什么新鲜的。我们向服务器发送了消息。你该熟悉这些代码,它们与异步调用的几乎一模一样。二者的区别在服务器代码中:
handle_call({deposit, Name, Amount}, _From, State) ->
case dict:find(Name, State) of
{ok, Value} ->
NewBalance = Value + Amount,
Response = {ok, NewBalance},
NewState = dict:store(Name, NewBalance, State),
{reply, Response, NewState};
error ->
{reply, {error, account_does_not_exist}, State}
end;
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
哇!不少新鲜货!函数定义看似与handle_cast相同,只是我们没用参数From。From是发送调用的进程的标识(pid),因而,必要时我们可以发送附加消息。
我们答应过ErlyBank,会核验银行帐户的存在与否,首先是我们在何处做这件事。我们试图从状态字典中,找到相关存款帐户的数值。字典的查找函数,返回{ok, Value}或者error。
如果该帐户存在,返回的变量Value就是它的存款余额,于是,我们加上新的存款数额。然后,我们把新的存款余额保存到字典中,并给变量赋值。我还把返回的值存在变量中,这个变量可看做对存款API的注释:{ok, Balance}。接着,返回元组 {reply, Reply, State},服务器把变量Reply返回发出调用的进程,并且保存服务器的新状态。
另一方面,如果该帐户不存在,我们就不改变服务器的状态,但要返回元组 {error, account_does_not_exist}。
增加了处理存款的API后,eb_server.erl 源文件更新如下:
%%%-------------------------------------------------------------------
%%% File : eb_server.erl
%%% Author : Mitchell Hashimoto <mitchell.hashimoto@gmail.com>
%%% Description : The ErlyBank account server.
%%%
%%% Created : 5 Sep 2008 by Mitchell Hashimoto <mitchell.hashimoto@gmail.com>
%%%-------------------------------------------------------------------
-module(eb_server).
-behaviour(gen_server).
%% API
-export([start_link/0,
create_account/1,
deposit/2]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
%%====================================================================
%% API
%%====================================================================
%%--------------------------------------------------------------------
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
%% Description: Starts the server
%%--------------------------------------------------------------------
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%%--------------------------------------------------------------------
%% Function: create_account(Name) -> ok
%% Description: Creates a bank account for the person with name Name
%%--------------------------------------------------------------------
create_account(Name) ->
gen_server:cast(?SERVER, {create, Name}).
%%--------------------------------------------------------------------
%% Function: deposit(Name, Amount) -> {ok, Balance} | {error, Reason}
%% Description: Deposits Amount into Name's account. Returns the
%% balance if successful, otherwise returns an error and reason.
%%--------------------------------------------------------------------
deposit(Name, Amount) ->
gen_server:call(?SERVER, {deposit, Name, Amount}).
%%====================================================================
%% gen_server callbacks
%%====================================================================
%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% Description: Initiates the server
%%--------------------------------------------------------------------
init([]) ->
{ok, dict:new()}.
%%--------------------------------------------------------------------
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} |
%% {stop, Reason, State}
%% Description: Handling call messages
%%--------------------------------------------------------------------
handle_call({deposit, Name, Amount}, _From, State) ->
case dict:find(Name, State) of
{ok, Value} ->
NewBalance = Value + Amount,
Response = {ok, NewBalance},
NewState = dict:store(Name, NewBalance, State),
{reply, Response, NewState};
error ->
{reply, {error, account_does_not_exist}, State}
end;
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
%%--------------------------------------------------------------------
%% Function: handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% Description: Handling cast messages
%%--------------------------------------------------------------------
handle_cast({create, Name}, State) ->
{noreply, dict:store(Name, 0, State)};
handle_cast(_Msg, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% Function: handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% Function: terminate(Reason, State) -> void()
%% Description: This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
ok.
%%--------------------------------------------------------------------
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
%% Description: Convert process state when code is changed
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
取款与帐户注销
取款与帐户注销的问题,我确实想留作练习,由读者完成该模块的API函数。你若需要dict模块的帮助,可参考它的API:
如果你完成了作业,或者放弃了(但愿没有!),你可以看以下答案。
增加了取款和帐户注销业务API后,eb_server.erl 源文件更新如下:
%%%-------------------------------------------------------------------
%%% File : eb_server.erl
%%% Author : Mitchell Hashimoto <mitchell.hashimoto@gmail.com>
%%% Description : The ErlyBank account server.
%%%
%%% Created : 5 Sep 2008 by Mitchell Hashimoto <mitchell.hashimoto@gmail.com>
%%%-------------------------------------------------------------------
-module(eb_server).
-behaviour(gen_server).
%% API
-export([start_link/0,
create_account/1,
deposit/2,
withdraw/2,
delete_account/1]).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
%%====================================================================
%% API
%%====================================================================
%%--------------------------------------------------------------------
%% Function: start_link() -> {ok,Pid} | ignore | {error,Error}
%% Description: Starts the server
%%--------------------------------------------------------------------
start_link() ->
gen_server:start_link({local, ?SERVER}, ?MODULE, [], []).
%%--------------------------------------------------------------------
%% Function: create_account(Name) -> ok
%% Description: Creates a bank account for the person with name Name
%%--------------------------------------------------------------------
create_account(Name) ->
gen_server:cast(?SERVER, {create, Name}).
%%--------------------------------------------------------------------
%% Function: deposit(Name, Amount) -> {ok, Balance} | {error, Reason}
%% Description: Deposits Amount into Name's account. Returns the
%% balance if successful, otherwise returns an error and reason.
%%--------------------------------------------------------------------
deposit(Name, Amount) ->
gen_server:call(?SERVER, {deposit, Name, Amount}).
%%--------------------------------------------------------------------
%% Function: withdraw(Name, Amount) -> {ok, Balance} | {error, Reason}
%% Description: Withdraws Amount from Name's account.
%%--------------------------------------------------------------------
withdraw(Name, Amount) ->
gen_server:call(?SERVER, {withdraw, Name, Amount}).
%%--------------------------------------------------------------------
%% Function: delete_account(Name) -> ok
%% Description: Deletes the account with the name Name.
%%--------------------------------------------------------------------
delete_account(Name) ->
gen_server:cast(?SERVER, {destroy, Name}).
%%====================================================================
%% gen_server callbacks
%%====================================================================
%%--------------------------------------------------------------------
%% Function: init(Args) -> {ok, State} |
%% {ok, State, Timeout} |
%% ignore |
%% {stop, Reason}
%% Description: Initiates the server
%%--------------------------------------------------------------------
init([]) ->
{ok, dict:new()}.
%%--------------------------------------------------------------------
%% Function: %% handle_call(Request, From, State) -> {reply, Reply, State} |
%% {reply, Reply, State, Timeout} |
%% {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, Reply, State} |
%% {stop, Reason, State}
%% Description: Handling call messages
%%--------------------------------------------------------------------
handle_call({deposit, Name, Amount}, _From, State) ->
case dict:find(Name, State) of
{ok, Value} ->
NewBalance = Value + Amount,
Response = {ok, NewBalance},
NewState = dict:store(Name, NewBalance, State),
{reply, Response, NewState};
error ->
{reply, {error, account_does_not_exist}, State}
end;
handle_call({withdraw, Name, Amount}, _From, State) ->
case dict:find(Name, State) of
{ok, Value} when Value < Amount ->
{reply, {error, not_enough_funds}, State};
{ok, Value} ->
NewBalance = Value - Amount,
NewState = dict:store(Name, NewBalance, State),
{reply, {ok, NewBalance}, NewState};
error ->
{reply, {error, account_does_not_exist}, State}
end;
handle_call(_Request, _From, State) ->
Reply = ok,
{reply, Reply, State}.
%%--------------------------------------------------------------------
%% Function: handle_cast(Msg, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% Description: Handling cast messages
%%--------------------------------------------------------------------
handle_cast({create, Name}, State) ->
{noreply, dict:store(Name, 0, State)};
handle_cast({destroy, Name}, State) ->
{noreply, dict:erase(Name, State)};
handle_cast(_Msg, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% Function: handle_info(Info, State) -> {noreply, State} |
%% {noreply, State, Timeout} |
%% {stop, Reason, State}
%% Description: Handling all non call/cast messages
%%--------------------------------------------------------------------
handle_info(_Info, State) ->
{noreply, State}.
%%--------------------------------------------------------------------
%% Function: terminate(Reason, State) -> void()
%% Description: This function is called by a gen_server when it is about to
%% terminate. It should be the opposite of Module:init/1 and do any necessary
%% cleaning up. When it returns, the gen_server terminates with Reason.
%% The return value is ignored.
%%--------------------------------------------------------------------
terminate(_Reason, _State) ->
ok.
%%--------------------------------------------------------------------
%% Func: code_change(OldVsn, State, Extra) -> {ok, NewState}
%% Description: Convert process state when code is changed
%%--------------------------------------------------------------------
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
%%--------------------------------------------------------------------
%%% Internal functions
%%--------------------------------------------------------------------
结论
我以本文向你展示了gen_server的基本用法,以及如何创建“客户-服务”关系。我没涉及gen_server的每个方面,如接收消息的超时设定和服务器的终止运行,但我解释了这一行为模块的实质性内容。
如果你要学习gen_server更多的知识,如回应函数、可能的返回值等更高级的用法,可以读相关手册:
还有,我没有触碰并很少提到函数code_change/3,而它的功能却是引人入胜的。别泄气,本系列文章最后几篇,有专门介绍。可以看到使用这个函数,对运行中的系统进行的实时更新。
下一篇文章,是关于 gen_fsm。