一:创建名称服务器
名称服务器
这种程序会返回一个给定名称的关联值。我们也可以修改某个名称所关联的值。
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
与外部世界通信的主要做法。