erlang学习笔记之OTP

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}.

运行结果

我们在这里实时更换了回调模块,这就是动态代码升级。

这个方法极其强大。传统上我们认为服务器是有状态的程序,当我们向它发送消息时会改变它的状态。服务器里的代码在首次调用时就固定了,如果想要修改服务器里的代码,就必须停止服务器并修改代码,然后重启服务器。在前面的例子中,修改服务器的代码就像修改服务器的状态那样简单。它不会因为软件维护升级而停止服务。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值