Erlang分布式编程和接口技术

一:创建名称服务器

名称服务器 这种程序会返回一个给定名称的关联值。我们也可以修改某个名称所关联的值。

1.一个简单的名称服务器

代码如下:
%% socket_dist/kvs.erl
-module(kvs).
-export([start/0,store/2,lookup/1]).

start() -> 
    register(kvs,spawn(fun() -> loop() end)).

store(Key,Value) -> rpc({store, Key, Value}).

lookup(Key) -> rpc({lookup,Key}).

rpc(Q) ->
    kvs ! {self(), Q},
    receive
        {kvs, Reply} ->
            Reply
    end.

loop() ->
    receive
        {From, {store, Key, Value}} ->
            put(Key, {ok, Value}),
            From ! {kvs, true},
            loop();
        {From, {lookup, Key}} ->
            From ! {kvs, get(Key)},
            loop()
    end.

运行这个服务器,最终结果为:

1> c(kvs).
{ok,kvs}

2> kvs:start().
true

3> kvs:store({location,joe},"stockholm").
true

4> kvs:store(weather, raining).
true

5> kvs:lookup(weather).
{ok,raining}

6> kvs:lookup({location, joe}).
{ok,"stockholm"}

7> kvs:lookup({location, jane}).
undefined

 2.客户端在一个节点,服务器在相同主机的另一个节点

现在在 同一台 计算机上启动两个 Erlang节点。为此,需要打开两个终端窗口,然后启动两套Erlang系统。首先,将开启一个终端shell,并在这个shell里启动一个名为gandalf的分布式Erlang节点。
然后启动服务器:
$ erl -sname gandalf
(gandalf@localhost) 1> kvs:start().
true
参数 -sname gandalf 的意思是“在本地主机上启动一个名为 gandalf Erlang 节点”。注意一下Erlang shell 是如何把 Erlang 节点名打印在命令提示符前面的。节点名的形式是 Name@Host 。 Name和 Host 都是原子,所以如果它们包含任何非原子的字符,就必须加上引号。下图是在win上运行的情况图

 

接下来将开启 第二个 终端会话,然后启动一个名为 bilbo Erlang 节点。这样就可以用库模块rpc 来调用 kvs 里的函数了。
$ erl -sname bilbo
(bilbo@localhost) 1> rpc:call(gandalf@localhost, kvs, store, [weather, fine]).
true

(bilbo@localhost) 2> rpc:call(gandalf@localhost, kvs, lookup, [weather]).
{ok,fine}

下图是在win上运行情况图:

注:测试时记得检查你的主机名,不是DESKTOP-N7GA4A8类型名字,如果是,可能在rpc:carll时会报N7GA4A8未找到变量的问题,当然,中文名也是可能会出现这样的哦!

虽然看起来不太起眼,但实际上已经执行了我们的第一次分布式计算!服务器运行在我们启动的第一个节点上,客户端则运行在第二个节点上。设置weather 值的调用是由 bilbo 节点发出的,可以切换回 gandalf 来检查一下天气(weather )的值。
(gandalfelocalhost) 2> kvs:lookup(weather).
{ok,fine}

下图是在win上运行情况图:

 

rpc:call(Node, Mod, Func, [Arg1, Arg2, ..., ArgN]) 会在 Node 上执行一次 远程过程调用。调用的函数是 Mod:Func(Arg1, Arg2, ..., ArgN) 。如你所见,这个程序的工作方式和非分布式Erlang 一致。唯一的区别在于客户端运行在一个节点上,而服务器运行在另一个不同的节点上。

3.同一局域网内不同机器上的客户端和服务器

我们将使用两个节点。第一个名为 gandalf 的节点在 doris.myerl.example.com 上,第二个名为bilbo 的节点在 george.myerl.example.com 上。开始工作之前,我们先用 ssh vnc 等工具在两台不同的机器上各启动一个终端。我们把这两个窗口称为doris george 。做完这些之后,我们就可以在两台机器上轻松输入命令了。

1步:是在doris上启动一个Erlang节点:

doris $ erl -name gandalf -setcookie abc
(gandalfedoris.myerl.example.com) 1>  kvs:start().
true

 2步:是在george上启动一个Erlang节点并向gandalf发送一些命令:

george $ erl -name bilbo -setcookie abc
(bilbo@george.myerl.example.com) 1> rpc:call(gandalf@doris.myerl.example.com,
kvs, store, [weather, cold]).
true

(bilboogeorge.myerl.example.com) 2> rpc:call(gandalf@doris.myerl.example.com,
kvs, Lookup, [weather]).
{ok,cold}

 它们的行为和同一机器上两个不同节点的情况完全一致。

4.跨互联网不同主机上的客户端和服务器

原则上,这和第 3 阶段是一样的,但现在我们必须更加关注安全性。运行同一局域网内的两个节点时,多半不会过于担心安全性。在大多数机构里,局域网都是通过防火墙与互联网隔离的。可以在防火墙后面自由分配临时IP 地址,对机器的设置也很随意。
要让系统准备好运行分布式 Erlang ,需执行以下步骤:
(1) 确保 4369 端口对 TCP UDP 流量都开放。这个端口会被一个名为 epmd 的程序使用(它是 Erlang Port Mapper Daemon的缩写,即 Erlang 端口映射守护进程)。
(2) 选择一个或一段连续端口给分布式 Erlang 使用,并确保这些端口是开放的。如果这些端口位于Min Max 之间(只想用一个端口就让 Min=Max ),就用以下命令启动 Erlang
$ erl -name ... -setcookie  ... -kernel inet_dist_listen_min Min \ inet_dist_listen_max Max

二: 远程分裂示例

为了展示如何在某个远程节点上分裂进程,可以编写下面代码:

%% dist_demo.erl
-module(dist_demo).
-export([rpc/4,start/1]).

start(Node) ->
    spawn (Node, fun() -> loop() end).

rpc(Pid, M, F, A) ->
    Pid ! {rpc,self(), M, F, A},
    receive
        {Pid,Response} ->
            Response
    end.

loop() ->
    receive
        {rpc, Pid, M, F, A} ->
            Pid ! {self(), (catch apply(M,F,A))},
            loop()
    end.
然后启动两个节点,它们都必须能够载入这段代码。如果这两个节点在同一台主机上,这就不成问题。只需从同一个目录里启动两个Erlang节点就可以了。如果节点分别属于两台物理隔离且文件系统不同的主机,这个程序就必须被复制到所有节点上,编译之后才能启动节点(或者也可以把.beam文件复制到所有节点上)。现在进行代码测试:
在主机 doris 上,启动一个名为 gandalf 的节点:
doris $ erl -name gandalf -setcookie abc
(gandalfodoris.myerl.example.com) 1> 

在主机george上,启动一个名为bilbo的节点,要记得使用同一个cookie:

george $ erl -name bilbo -setcookie abc
(bilboegeorge.myerl.example.com) 1>

 现在(在bilbo上),让远程节点(gandalf)分裂一个进程:

(bilboegeorge.myerl.example.com)1> Pid=dist_demo:start('gandalf@doris.myerl.example.com').
<5094.40.0>
现在, Pid 是这个 远程节点 进程的标识符,调用 dist_demo:rpc/4 ,在远程节点上执行一次远程过程调用:
(bilbo@george.myerl.example.com) 2> dist_demo:rpc(Pid,erlang,node,[]).
'gandalf@doris.myerl.example.com'
它在 远程节点上 执行 erlang:node() 并返回一个值。

三:服务器代码

首先来编写一个配置文件:
{port,1234).
{service,nameserver,password,"ABXy45", 
    mfa,mod_name_server,start_me_up,notUsed}.
它的意思是我们将在自己机器的 1234 端口上提供一个名为 nameServer 的服务。这个服务被密码ABXy45保护。当客户端调用下面的函数来创建连接时:
connect(Host,1234,nameServer,"ABXy45",nil).
服务器会分裂 mod_name_server:start_me_up(MM, nil, notUsed) MM 是一个代理进程的PID ,用来和客户端通信。下面是一个服务端的代码:
%% socket_dist/mod_name_server.erl
-module(mod_name_server).
-export([start_me_up/3]).

start_me_up(MM, _ArgsC, _Args) ->
    loop(MM).

loop(MM) ->
    receive
        {chan, MM, {store, K, V}} ->
            kvs:store(K, V),
            loop(MM);
        {chan, MM, {lookup,K}} ->
            MM ! {send,kvs:lookup(K)},
            loop(MM);
        {chan_closed, MM} ->
            true
    end.
为了测试这段代码,首先会确保它能在单台机器上正常工作,现在可以启动名称服务器(和kvs 模块)了。测试方法和上面差不多。

四:用端口建立外部 C 程序接口

我们将从一些简单的 C 代码开始。 example1.c 包含了两个函数。第一个函数计算两个整数之和,第二个函数计算参数的两倍是多少,如下所示:
 // ports/example1.c
int sum(int x,int y){
    return x+y;
}

int twice(int x){
    return 2*x;
}
我们的最终目的是从 Erlang里调用这些方法。要实现它,需要把sum(12,23)和twice(10)这样的函数调用转变成字节序列,通过端口发送给外部程序。端口给字节序列加上长度信息,然后把结果发给外部程序。当外部程序回复时,端口接收回复,并把结果发给与端口相连的进程。

1.C程序

C 程序有三个文件。
(1)  example1.c :包含了我们想要调用的函数(之前已经见过它了)。
(2)  example1_driver.c :管理字节流协议并调用 example1.c 里的方法。
(3)  erl_comm.c :带有读取和写入内存缓冲区的方法。
(1)  example1_driver.c这段代码有一个循环,它会从标准输入读取命令,调用应用程序的方法,后把结果写入标准输出。
// ports/example1_driver.c
#include <stdio.h>
#include <stdlib.h>
typedef unsigned char byte;

int read_cmd(byte *buff);
int write cmd(byte *buff, int len);
int sum(int x,int y);
int twice(int x);

int main(){
    int fn, argl, arg2, result;
    byte buff[100];

    while (read cmd(buff) > 0){
        fn = buff[0];

        if(fn == 1){
            argl = buff[1];
            arg2 = buff[2];

            /* 调试,可以打印到stderr.来调试
                fprintf(stderr, "calling sum %i %in",argl,arg2); */
            result sum(argl,arg2);
        }else if (fn == 2) {
            argl buff[1];
            result twice(argl);
        }else {
            /* 碰到未知函数就直接退出 */
            exit(EXIT_FAILURE);
        }
        buff[0] = result;
        write_cmd(buff, 1);
    }
}

 (2)erl_comm.c最后是从标准输入和输出里读取和写入数据的代码。这段代码允许数据出现可能的分块。

// ports/erl_comm.c
/*erl comm.c */
#include <unistd.h>
typedef unsigned char byte;

int read_cmd(byte *buf);
int write_cmd(byte *buf,int len);
int read_exact(byte *buf,int len);
int write_exact(byte *buf,int len);

int read_cmd(byte *buf)
{
    int len;
    if (read_exact(buf, 2)!= 2)
        return(-1);
    len = (buf[0] << 8) | buf[1]:
    return read_exact(buf,len);
}
int write_cmd(byte *buf, int len)
{
    byte li;
    1i=(1en>> 8) & 0xff;
    write_exact(&li, 1);
    li = len & 0xff;
    write_exact(&li, 1);
    return write_exact(buf, len);
}
int read_exact(byte *buf, int len)
{
int i, got=0;
    do {
        if ((i = read(0, buf+got, len-got)) <= 0)
            return(i);
        got += i;
    }while (got<len);
    return(len);
}
int write_exact(byte *buf, int len)
{
    int i,wrote =0;
    do {
        if ((i = write(1, buf+wrote, len-wrote)) <= 0)
            return (i);
        wrote +=i;
   } while (wrote < len);
    return (len);
}
这段代码专门用于处理带有 2 字节长度包头的数据,因此它与提供给端口驱动程序的 {packet, 2}选项匹配。

2.Erlang程序

端口的 Erlang 一侧由下面这个程序驱动:
%% ports/example1.erl
-module(examplel).
-export([start/0,stop/0]).
-export([twice/1,sum/2]).

start() ->
    register(examplel,
        spawn (fun() ->
                process_flag(trap_exit, true),
                Port = open_port({spawn,"./examplel"}, [{packet,2}]),
                loop(Port)
               end)).

stop() ->
    ?MODULE ! stop.

twice(X) -> call_port({twice, X}).

sum(X, Y) -> call_port({sum, X, Y}).

call_port(Msg) ->
    ?MODULE ! {call,self(), Msg},
    receive
        {?MODULE, Result} ->
            Result
    end.

loop(Port) ->
    receive
        {call, Caller, Msg} ->
            Port ! {self(), {command,encode(Msg)}},
            receive
                {Port, {data, Data}} ->
                    Caller ! {?MODULE, decode(Data)}
            end,
            loop(Port);
            stop ->
                Port ! {self(), close},
            receive
                {Port, closed} ->
                    exit(normal)
            end;
        {'EXIT',Port,Reason} ->
            exit({port_terminated, Reason})
    end.

encode({sum, X, Y}) -> [1, X, Y];
encode({twice, X}) -> [2, X].

decode([Int]) -> Int.
这段代码遵循一个相当标准的模式。我们在 start/0 里创建了一个名为 example1 的注册进程(服务器)。call_port/1 实现了对服务器的远程过程调用。 twice/1 sum/2 是接口方法,它们必须被导出,会对服务器发起远程过程调用。我们在loop/1里编码了对外部程序的请求,并将来自外部程序的返回值做了适当处理。程序已经编写完成。现在需要的就是一个构建程序的 makefile

3.编译和链接端口程序

这个 makefile 会编译并链接本章描述的端口驱动和内链驱动程序,以及所有相关的 Erlang 代码。这个makefile 只在 Mac OS X “美洲狮”系统上测试过,其他操作系统需要进行修改。它还包含一个小型测试程序,每次代码重建时都会运行。
<!-- ports/Makefile.mac -->
.SUFFIXES: .erl .beam .yrl

.erl.beam:
    erlc -W $<

MODS = examplelexamplel lid unit test

all:    ${MODS:%=%.beam}examplel examplel_drv.so
       @erl -noshell -s unit_test start
examplel: examplel.c erl comm.c examplel_driver.c
       gcc -o examplel examplel.c erl_comm.c examplel_driver.c
examplel_drv.so: examplel lid.c examplel.c
       gcc -arch i386-I /usr/local/lib/erlang/usr/include\
           -o examplel_drv.so -fPIC -bundle -flat_namespace -undefined suppress\
           examplel.c examplel lid.c
clean:
       rm examplel examplel drv.so *.beam

现在就可以运行程序了:

1> examplel:start().
true

2> examplel:sum(45,32).
77

4> examplel:twice(10).
20

...
这样就完成了我们的第一个范例端口程序。这个程序实现的端口协议是 Erlang 与外部世界通信的主要做法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明明如皓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值