Outline
笔记系列
Erlang环境和顺序编程
Erlang并发编程
Erlang分布式编程
Yaws
Erlang/OTP
日期 变更说明
2014-11-23 A Outline
A 1.1-1.2
2014-12-08 A 1.3
2014-12-13 A 2, 3
2014-12-14 A 4
Agenda
写在前面
Erlang研磨和技术细节,可以移步园中http://www.cnblogs.com/me-sa/。如果一开始我就可以看到他的笔记的话,...。
0范围
节点和通信
基本分布式编程模块
empd进程
套接字编程
1 Erlang节点和通信
1.1节点
一个Erlang节点是已命名的(named)的正在运行的Erlang运行时系统(erts)。
多个节点可以运行在一台机器上,也可以运行在不同的机器上。之前的顺序编程、并发编程中示例实际上是在一个Erlang节点上运行的。
存活节点/可命名的节点:如果一个节点可以与其他节点通信。任何存活的节点都需要命名,命名有两种方式:
(1)短名字 erl -sname <sname>
sname在局域网中命名一个主机,已name@host给出,例:foo@zhoujiagen
(2)长名字 erl -name <name>
name给出完整的主机IP地址,可以是foo@192.168.1.103,或foo@zhoujiagen(可DNS解析的主机名)。
长名字节点只能与长名字节点通信,短名字节点只能与短名字节点通信。
节点启动和信息
erl -[s]name <nodeName>
命名节点方式启动
net_kernel:start([‘foo@zhoujiagen’]).
启动节点
net_kernel:stop().
停止当前节点
node().
查看当前节点
elrang:is_alive().
当前节点是否存活
1.2 节点通信
节点连通性测试
net_adm:ping(‘bar@zhoujiagen’).
pang:不连通,pong:连通。
Cookie
每个节点在任意时刻只有一个cookie,共享同一值的节点可以通信。
启动时设置cookie
erl -sname foo -setcookie <cookieValue>
运行时设置cookie
erlang:set_cookie(node(), <cookieValue>).
局限性
分布式节点通过cookie与另一远程节点建立连接后,远程节点的拥有者获得本地节点运行者用户相同的权限。远程节点能够执行spawn(YourNode, os, cmd, [“rm -rf *”]).是任何人都不想看到的。故,cookie安全通信在封闭式系统中是足够的,但在开放式系统中需要融入其他的安全通信机制,可以考虑安全套接字等。
连接性建立和配置
只要分布式Erlang节点共享相同的cookie值,它们之间就可以通信。
Erlang运行时系统(erts)在第一次引用一个节点时,自动建立连接。自动建立连接可以是通过调用net_adm:ping/1或者发送一个消息到该节点注册的进程上来完成。
连接在一起的节点,信息是共享的,具有可传递性。
net_kernel进程
每个节点的net_kernel进程负责协调分布式Erlang节点之间的操作,例如:spawn/4会被net_kernel进程转换为消息发送到远程节点的net_kernel进程上、net_kernel进程处理cookie认证。很重要的一点:用户可以修改net_kernel进程以获得期望的行为。
自动建立的可传递的连接行为的覆盖方法有:使用net_kernel模块中的函数手动的控制连接、运行erl -connect_all false拒绝节点全局的相互连接。
隐藏节点
希望覆盖默认配置下所有节点互相连接行为,只在必要时建立与其他节点的连接的时候,可以考虑隐藏节点。
启动隐藏节点: erl -sname ‘nodeName’ -hidden。
手动建立节点连接:net_kernel:connect(NodeName).。
nodes/0不会返回隐藏节点,nodes(hidden)返回隐藏节点,nodes(connected)返回隐藏节点和非隐藏节点。
1.3 远程过程调用与本地调用的区别
►Sample: facserver/facclient[1]
facserver.erl
-module(facserver). %% API -export([start/0]). %%%=================================================================== %%% API %%%=================================================================== start() -> register(facserver, self()), facLoop(). %%%=================================================================== %%% Internal functions %%%=================================================================== facLoop() -> receive {Pid, N} -> Pid ! {ok, fac(N)} end, facLoop(). fac(0) -> 1; fac(1) -> 1; fac(N) -> N * fac(N-1).
facclient.erl
-module(facclient). %% API -export([remote_call/2]). %%%=================================================================== %%% API %%%=================================================================== remote_call(Message, Node) -> {facserver, Node} ! {self(), Message}, %◄向远程节点远程发送消息节点 receive {ok, Result} -> Result end. %%%=================================================================== %%% Internal functions %%%===================================================================
运行示例
分布式系统是一个会因为另一个你甚至不知道其存在的计算机的错误,而导致你的计算机无法使用的系统。
—— Lesilie Lamport
远程过程调用与本地调用的本质区别在于:远程节点可能失效,无法调用期望的功能。故,需要在远程过程调用时添加一些“护卫”语句,以确保远程节点连接正常、远程节点失效时有机会优雅的继续处理。[1]中给出了三种常见的方法:超时(receive … after)、连接到远程节点进程(spawn_link/4)、监视远程节点进程(monitor_node(Node, Bool));有关细节可以参考Erlang随版本发布的文档。
2 基本分布式编程模块
2.1 erl模块
包括erl命令,用于启动Erlang运行时系统(erts),一些启动标志可以改变ert的行为:
-connect_all false
系统不维护连接节点的全局列表,即阻止全局声明。
-hidden
启动一个隐藏节点。
-name Name / -sname Name
设置节点名称。
-setcookie Cookie
设置节点的cookie。
2.2 rpc模块
rpc模块包含类似于远程过程调用的服务、广播和并行计算等功能。远程过程调用用于收集远程节点上的信息、或在远程节点上执行一些有副作用的功能。
call(Node, Module, Function, Args [, Timeout]) -> Res | {badrpc, Reason}
在节点Node上执行apply(Module, Function, Args),带超时的call/5会spawn出临时进程接收超时的回复。
block_call(Node, Module, Function, Args [, Timeout]) -> Res | {badrpc, Reason}
类似与call,但节点Node上RPC server不会创建独立的进程处理请求。
async_call()Node, Module, Function, Args) -> Key
实现流式调用承诺,调用方不会阻塞。返回的Key可以视为回复的承诺,后续用yield/1或nb_yield/1,2获取Res。
2.3 erlang模块
Erlang的BIF集合,涉及分布式编程的函数有:
erlang:set_cookie(Node, Cookie)
设置节点的cookie值。
erlang:get_cookie()
返回当前节点的cookie值,当前节点不活时返回nocookie。
node()
返回当前节点的Name@Host,不活时返回nonode@nohost。
node(Arg)
返回Arg所在的节点,Arg可以是pid、reference或port。
注:port的概念参见” Erlang Reference Manual User's Guide” §14,简单的说,port是Erlang与外部世界通信机制抽象。
nodes()
返回系统中可见的其他节点。
spawn(Node, Module, Function, ArgumentList)
在节点Node上执行spawn(Module, Function, ArgumentList)。
disconnect_node(Node)
断开与节点Node的连接。
monitor_node(Node, Flag)
打开或关闭对节点Node的监控。
2.4 net_kernel模块
net_kernel模块包含对手动起停、连接和监控节点的构造。默认情况下由erts自动调用,但用户可以通过修改来获得预期的行为。
例:connect_node(Node) -> boolean() | ignored
建立与节点Node的连接。
至于如何修改配置覆盖默认行为,待对erts有深入了解后再做探讨。
2.5 net_adm模块
Erlang的网络管理模块,包括ping等。
例:ping(Node) -> pong | pang
尝试建立与节点Node的连接,失败返回pang,成功返回pong。
3 epmd进程
epmd守护进程在参与分布式Erlang计算的主机上充当名称服务器,每个Erlang节点启动时,会获得一个名称和一个主机OS内核中的地址,在TCP/IP环境中,该地址是ip:port。节点将获得的名称和地址发送给本机上的empd守护进程。epmd负责将符号节点名称映射到地址上。
epmd监听的默认端口是4369。
详细内容参见” Erlang Run-Time System Application (ERTS) Reference Manual ” COMMAND empd。
4 Erlang套接字编程
操作系统视角的套接字(socket)阐述参见[3],这里不再班门弄斧,只做必要的说明。
值得特别说明的是下面的示例依赖于类似于UNIX DOMAIN Socket的机制,即单个主机内部应用程序之间通过Socket通信。
4.1 UDP
这里只提供一个交互示例,来源于[1],为体现Socket与节点通信的异同,UDP对偶端分别运行在两个本地节点上。
(1)首先启动两个本地节点、查看节点连通性
(2) 在foo节点上开启1234 UDP监听端口,在bar节点上开启1235 UDP监听端口
netstat一下
(3) 尝试发送一些消息/数据,再查看一下节点连通性
对偶端也来一次
(4) 关闭UDP Socket,inet:i()用于列举套接字
(5) 现在看看节点连通时发生了什么
原来也是用Socket通信的。
4.2 TCP
示例来源于[1],TCP server、client之间简单的通信。
►Sample: tcp_demo [1]
-module(tcp_demo). %% API -export([client/2, server/0, wait_connect/2]). %%%=================================================================== %%% API %%%=================================================================== client(Host, Data) -> {ok, Socket} = gen_tcp:connect(Host, 1234, [binary, {packet, 0}]), send(Socket, Data), ok = gen_tcp:close(Socket). server() -> {ok, ListenSocket} = gen_tcp:listen(1234, [binary, {active, false}]), wait_connect(ListenSocket, 0). wait_connect(ListenSocket, Count) -> {ok, Socket} = gen_tcp:accept(ListenSocket), % 成为控制进程 spawn(?MODULE, wait_connect, [ListenSocket, Count+1]), % 生成新的监听进程 get_request(Socket, [], Count). %%%=================================================================== %%% Internal functions %%%=================================================================== send(Socket, <<Chunk:100/binary, Rest/binary>>) -> gen_tcp:send(Socket, Chunk), send(Socket, Rest); send(Socket, Rest) -> gen_tcp:send(Socket, Rest). get_request(Socket, BinaryList, Count) -> case gen_tcp:recv(Socket, 0, 5000) of {ok, Binary} -> get_request(Socket, [Binary | BinaryList], Count); {error, closed} -> handle(lists:reverse(BinaryList), Count) end. handle(Binary, Count) -> {ok, Fd} = file:open("log_file.log", [write, append]), file:write(Fd, "Incoming " ++ integer_to_list(Count) ++ ":\n"), file:write(Fd, Binary ++ "\n\n"), file:close(Fd).
执行
(1)启动server
(2)使用client发送数据
(3)查看端口
(4)查看输出文件
(5)关闭客户端后查看端口
4.3 常见模块说明
4.3.1 gen_udp模块
UDP库模块,有关选项参数这里不列出,可以参考各模块的文档。
打开socket
gen_udp:open(Port) -> {ok, Socket} | {error, Reason}
gen_udp:open(Port, OptionList) -> {ok, Socket} | {error, Reason}
发送数据
gen_udp:send(Socket, Address, Port, Packet) -> ok | {error, Reason}
接收数据
gen_udp:recv(Socket, Length) -> {ok, {Address, Port, Packet}} | {error, Reason}
gen_udp:recv(Socket, Length, Timeout) -> {ok, {Address, Port, Packet}} | {error, Reason}
管理socket
gen_udp:close(Socket) -> ok
4.3.2 gen_tcp模块
TCP库模块。
启动监听socket
gen_tcp:listen(PortNumber, Options)
开始监听
gen_tcp:accept(Socket)
gen_tcp:accept(Socket, Timeout)
请求连接
gen_tcp:connect(Address, Port, OptionList) -> {ok, Packet} | {error, Reason}
gen_tcp:connect(Address, Port, OptionList, Timeout) -> {ok, Packet} | {error, Reason}
发送数据
gen_tcp:send(Socket, Packet) -> ok | {error, Reason}
被动模式下读取数据({active, false})
gen_tcp:recv(Socket, Length) -> {ok, Packet} | {error, Reason}
gen_tcp:recv(Socket, Length, Timeout) -> {ok, Packet} | {error, Reason}
raw模式的socket,简单的说应用程序不通过传输层,直接使用IP层数据 [3]。
关闭socket
gen_tcp:close(Socket) -> ok
控制进程和控制传递
控制进程:通过gen_tcp:accept或者gen_tcp:connect建立连接的进程。
控制传递:控制进程调用gen_tcp:controlling_process(Socket, Pid) -> ok | {error, Reason}。
请求处理的两种方式:
wait_connect(ListenSocket, Count) -> {ok, Socket} = gen_tcp:accept(ListenSocket), % 成为控制进程 spawn(?MODULE, wait_connect, [ListenSocket, Count+1]), % 生成新的监听进程 get_request(Socket, [], Count).
=>
wait_connect(ListenSocket, Count) -> {ok, Socket} = gen_tcp:accept(ListenSocket), % 成为控制进程 Pid = spawn(?MODULE, get_request, [Socket, [], Count]), % 生成新的控制进程 gen_tcp:controlling_process(Socket, Pid), wait_connect (ListenSocket, Count+1). % 继续等待/接收数据
4.3.3 inet模块
inet模块提供访问TCP/IP的方法,包含处理socket的一些函数。
获取和设置socket配置
inet:getopts(Socket, Options) -> {ok, OptionValues} | {error, posix()}
inte:setopts(Socket, Options) -> ok | {error, posix()}
获取socket统计数据
inte:getstat(Socket) -> {ok, OptionValues} | {error, posix()}
inte:getstat(Socket, Options) -> {ok, OptionValues} | {error, posix()}
一些有用的函数
inet:gethostname() 查看当前主机名称
inet:i() 查看已创建的所有socket信息
参考文献
[1] Cesarini F., Thompson S.著,慕尼黑Isar工作组 杨剑译.
Erlang编程指南.
北京: 机械工业出版社.2011.
[2] Armstrong J.著,牛化成 译.
Erlang程序设计(第2版).(Programming Erlang, Second Edition – Software for a Concurrent World).
北京: 人民邮电出版社.2014.
[3] Kerrisk M.著,郭光伟 陈舸 译.
Linux/UNIX系统编程手册(下册).
北京: 人民邮电出版社.2014.