OTP简介
OTP是Open Telecom Platform(开放电信平台)。但是现在OTP比你想的作用更多。
它是一个应用程序操作系统,包含了一组库和实现方式,可以构建大规模、容错和分布式的应用程序。标准的Erlang分发套装包含OTP库。 OTP包含了许多强大的工具,例如一个完整的Web服务器,一个FTP服务器和一个CORBA ORB等,它们全都是用Erlang编写的。
OTP还包含了构建电信应用程序的最先进工具,能够实现 H248、SNMP和ASN.1/Erlang交叉编译器(这些是电信行业里常用的协议)。
核心概念是OTP行为。该行为封装了常见的行为模式,你可以把它看作是一个用回调函数作为参数的应用程序框架。 OTP的威力来自于行为本身就能提供容错性、可扩展性和动态代码升级等属性。 简单地说,行为负责解决问题的非函数部分,而回调函数负责解决函数部分。
这么做的好处在于问题的非函数部分(比如如何进行实时代码升级)对所有应用程序都是一样的,而函数部分 (由回调函数提供)在每个问题里都是不同的。
从一个简单的服务器入手,了解gen_server模块,然后一步步改进它,直到实现gen_server模块的完整功能。
Server 1:基本的服务器
%% API
-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.
这段代码的回调函数
%% API
-export([add/2, find/1, init/0, handle/2]).
-import(server1, [rpc/2]).
%% 客户端方法
add(Name, Place) -> rpc(name_server, {add, Name, Place}).
find(Name) -> rpc(name_server, {find, Name}).
%% 回调方法
init() -> dict:new().
handle({add, Name, Place}, Dict) -> {ok, dict:store(Name, Place, Dict)};
handle({find, Name}, Dict) -> {dict:find(Name, Dict), Dict}.
这段代码实际上执行两个任务。它首先充当被服务器框架代码调用的回调模块,与此同时, 它还包含了将被客户端调用的接口方法。OTP的惯例是把这两类函数放在同一个模块里。
运行代码
这只是一个纯粹的顺序代码,没有分裂,没有发送,没有接收,也没有注册。但这也是所有服务器的基本模式
Server 2:实现事务的服务器
%% API
-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]).
这段代码在服务器里实现了“事务语义”,它会在处理函数抛出异常错误时用State(状态) 的初始值继续循环。
但如果处理函数成功了,它就会用处理函数提供的NewState值继续循环。 当处理函数失败时,服务器会给发送问题消息的客户端发送一个消息,让它崩溃。这个客户端不能继续工作,因为它发送给服务器的请求导致了处理函数的崩溃,但其他想要使用服务器的客户端不会受到影响。
当处理函数发生错误时,服务器的状态不会改变。这个服务器使用的回调模块和用于server1的回调模块一模一样。通过修改服务器并保持回调模块不变,我们就能修改回调模块的非函数行为。
Server 3:实现热代码交换的服务器
%% API
-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.
如果向服务器发送一个交换代码消息,它就会把回调模块改为消息里包含的新模块。 我们可以演示这一点,做法是用某个回调模块启动server3,然后动态交换这个回调模块。 不能用name_server作为回调模块,因为服务器名已经被硬编译进这个模块里了。因此,将制作一个名为name_server1的副本,然后在里面修改服务器的名称
%% API
-export([add/2, find/1, init/0, handle/2]).
-import(server1, [rpc/2]).
%% 客户端方法
add(Name, Place) -> rpc(name_server, {add, Name, Place}).
find(Name) -> rpc(name_server, {find, Name}).
%% 回调方法
init() -> dict:new().
handle({add, Name, Place}, Dict) -> {ok, dict:store(Name, Place, Dict)};
handle({find, Name}, Dict) -> {dict:find(Name, Dict), Dict}.
运行结果
现在假设想要找出这个名称服务器能提供的所有名称。API里没有函数能做到这一点,因为 name_server模块只包含访问函数add和find。
一个新的回调模块
%% API
-export([all_names/0, delete/1, init/0, add/2, find/1, init/0, handle/2]).
-import(server1, [rpc/2]).
%% 接口
all_names() -> rpc(name_server, allNames).
add(Name, Place) -> rpc(name_server, {add, Name, Place}).
delete(Name) -> rpc(name_server, {delete, Name}).
find(Name) -> rpc(name_server, {find, Name}).
%% 回调方法
init() -> dict:new().
handle({add, Name, Place}, Dict) -> {ok, dict:store(Name, Place, Dict)};
handle({all_names, Dict}, Dict) -> {dict:fetch_keys(Dict)};
handle({delete, Name}, Dict) -> {ok, dict:erase(Name, Dict)};
handle({find, Name}, Dict) -> {dict:find(Name, Dict), Dict}.
运行结果
我们在这里实时更换了回调模块,这就是动态代码升级。
这个方法极其强大。传统上我们认为服务器是有状态的程序,当我们向它发送消息时会改变它的状态。服务器里的代码在首次调用时就固定了,如果想要修改服务器里的代码,就必须停止服务器并修改代码,然后重启服务器。在前面的例子中,修改服务器的代码就像修改服务器的状态那样简单。它不会因为软件维护升级而停止服务。