并发编程
使用并发
spawn创建一个并行进程, send向某个进程发送消息, receive则是接收消息
Erlang的并发是基于进程( process)的。进程是一些独立的小型虚拟机,可以执行Erlang函数 ,在Erlang里,进程隶属于编程语言,而非操作系统。
Erlang中的进程会执行某个特点方法,并且执行过程是以一个并发的形式执行的
spawn(Mod, Func, Args) -> Pid,进程标识符 ,以用Pid来给此进程发送消息
可以使用 Pid ~Message 的形式给进程Pid发送消息
使用receive … of 来接收消息
rpc(Pid, Request) ->
Pid ! {self(), Request},
receive
{Pid, Response} ->
Response
end.
loop() ->
receive
{From, {rectangle, Width, Ht}} ->
From ! {self(), Width * Ht},
loop();
{From, {circle, R}} ->
From ! {self(), 3.14159 * R * R},
loop();
{From, Other} ->
From ! {self(), {error,Other}},
loop()
end.
self()是客户端进程的标识符 ,可以用来获取当前进程的Pid
发送请求的进程通常称为客户端。接收请求并回复客户端的进程称为服务器。
睡眠Sleep
编写一个只有超时部分的receive。 通过这种方法,我们可以定义一个sleep(T)函数,它会让当前的进程挂起T毫秒
sleep(T) ->
receive
after T ->
ok
end.
如果接收语句里的超时值是原子infinity(无穷大),就永远不会触发超时
定时器
可以用接收超时来实现一个简单的定时器
函数stimer:start(Time, Fun)会在Time毫秒之后执行Fun(一个不带参数的函数)。它返回一个句柄(是一个PID),可以在需要时用来关闭定时器
-module(stimer).
-export([start/2, cancel/1]).
start(Time, Fun) -> spawn(fun() -> timer(Time, Fun) end).
cancel(Pid) -> Pid ! cancel.
timer(Time, Fun) ->
receive
cancel ->
void
after Time ->
Fun()
end.
注册进程
Erlang有一种公布进程标识符的方法,它让系统里的任何进程都能与该进程通信。这样的进
程被称为注册进程( registered process)。
register(AnAtom, Pid)
用AnAtom( 一个原子)作为名称来注册进程Pid。如果AnAtom已被用于注册某个进程,这次注册就会失败
register(demo, spawn(?MODULE, loop,[])),
demo ! {msg}.
使用了注册进程之后,可以直接用注册进程原子名称进行发送消息,可以不需要Pid标识符
unregister(AnAtom)
移除与AnAtom关联的所有注册信息
whereis(AnAtom)
检查AnAtom是否已被注册。如果是就返回进程标识符Pid, 如果没有找到与AnAtom关联的进程就返回原子undefined。
registered() ->[AnAtom::atom()]
返回一个包含系统里注册进程的列表,注意是列表
分布式
在分布式Erlang里,我们编写的程序会在Erlang的节点( node)上运行。 节点包含自带地址空间和进程组的完整虚拟机
%% 启动某个节点
erl -sname 节点名称
接口:
在一个节点上调用执行一个函数
rpc:call(Node, Module, Function, Args) -> Res | {badrpc, Reason}
Node:节点的地址,如节点名称demo,则 demo@localhost
Module, Function, Args就跟apply的(Module, Function, Args)类似
通过rpc:call就能够远程调用服务
为了让两台不同网络的节点能够互通,因此需要在启动到时候设置相同的cookies
erl -sname test1 -setcookie abc
erl -sname test2 -setcookie abc
cookie系统让访问单个或一组节点变得更安全。每个节点都有一个cookie,如果它想与其他任何节点通信,它的cookie就必须和对方节点的cookie相同
cookie从不会在网络中明文传输,它只用来对某次会话进行初始认证。分布式Erlang会话不是加密的,但可以被设置成在加密通道中运行
套间字编程
gen_tcp用于编写TCP应用程序, gen_udp用于编写UDP应用程序
TCP
gen_tcp:connect
connect(Address, Port, Options, Timeout) 连接一个 TCP 端口
返回: {ok, Socket} | {error, Reason}
用给出的端口 Port 和 IP 地址 Address 连接到一个服务器上的 TCP 端口上。参数 Address 即可以是一个主机名,也可以是一个 IP 地址。参数 Timeout 指定一个以毫秒为单位的超时值,默认值是 infinity。
连接localhost:8888 端口
{ok, Socket} = gen_tcp:connect("localhost", 8888, [binary, {packet, 4}]),
连接调用里的binary参数告诉系统要以“二进制”模式打开套接字,并把所有数据用二进制型传给应用程序。
{packet,4}的意思是把未经修改的TCP数据直接传给应用程序,即每个逻辑请求或响应前面都会有一个4字节的长度计数,这个数字是客户端和服务端双方约定的
packet这个词在这里指的是应用程序请求或响应消息的长度,而不是网络上的实际数据包
客户端和服务器使用的packet参数必须一致。如果启动服务器时用了{packet,2},客户端用了{packet,4}, 程序就会失败
gen_tcp:send
在一个套接字 Socket 发送一个数据包
用法:
send(Socket, Packet) -> ok | {error, Reason}
代码:
Packet是请求的Request,要转为二进制型来操作
ok = gen_tcp:send(Socket, term_to_binary({Pid = self(), login})),
receive
在gen_tcp:send之后,需要{tcp,Socket,Bin} 来接收消息
Bin是二进制型,需要手动调用方法binary_to_term转为tuple
loop(Socket) ->
receive
{tcp, Socket, Bin} ->
case binary_to_term(Bin) of
{Pid, login} -> login(Socket, Pid);
{Pid, logout} -> logout(Socket, Pid)
end;
{tcp_closed, Socket} ->
stop
end.
收到一个{tcp_closed, Socket}消息。这会在服务器完成数据发送时发生,意味着连接已经关闭
gen_tcp:listen
开启一个监听某个端口的套接字,在本地开启一个监听某个端口的套接字(socket)。开启成功的话,会返回一个套接字标识符 Listen,其一般会传递给 get_tcp:accept/1 或 get_tcp:accept/2 调用。
用法:
listen(Port, Options) -> {ok, Listen} | {error, Reason}
参数 Options 的一些常用选项:
- {active, true}:套接字设置为主动模式。所有套接字接收到的消息都作为 Erlang 消息转发到拥有这个套接字进程上。当开启一个套接字时,默认是主动模式。
- {active, false}:设置套接字为被动模式。套接字收到的消息被缓存起来,进程必须通过调用函数 gen_tcp:recv/2 或 gen_tcp:recv/3 来读取这些消息。
- {active, once}:将设置套接字为主动模式,但是一旦收到第一条消息,就将其设置为被动模式,并使用 **gen_tcp:recv/**2 或 gen_tcp:recv/3 函数来读取后续消息。
- {keepalive, true}:当没有转移数据时,确保所连接的套接字发送保持活跃(keepalive)的消息。因为关闭套接字消息可能会丢失,如果没有接收到保持活跃消息的响应,那么该选项可确保这个套接字能被关闭。默认情况下,该标签是关闭的。
- {nodelay, true}:数据包直接发送到套接字,不过它多么小。在默认情况下,此选项处于关闭状态,并且与之相反,数据被聚集而以更大的数据块进行发送。
- {packet_size, Size}:设置数据包允许的最大长度。如果数据包比 Size 还大,那么将认为这个数据包无效。
- {packet, 0}:表示 Erlang 系统会把 TCP 数据原封不动地直接传送给应用程序
- {reuseaddr, true}:允许本地重复使用端口号
- {nodelay, true}:意味着很少的数据也会被马上被发送出去
- {delay_send, true}:数据不是立即发送,而是存到发送队列里,等 socket 可写的时候再发送
- {backlog, 1024}: 缓冲待处理连接队列的最大长度,默认为5
- {exit_on_close, false}:设置为 flase,那么 socket 被关闭之后还能将缓冲区中的数据发送出去
- {send_timeout, 15000}:设置一个时间去等待操作系统发送数据,如果底层在这个时间段后还没发出数据,那么就会返回 {error,timeout}
Options是一个列表,里边存放以上参数,多个参数用逗号隔开,如:
{ok,Listen} = gen_tcp(8080, [ binary, {packet, 4}, {reuseaddr, true}, {active, true} ]).
gen_tcp:accept
接受一个套接字 ListenSocket 上的连接请求, ListenSocket 必须是由函数 gen_tcp:listen/2 建立返回
该函数会引起进程阻塞,直到有一个连接请求发送到监听的套接字。
用法:
accept(ListenSocket) -> {ok, Socket} | {error, Reason}
如果是使用了{active, true},则接收的方式:
loop(Socket) ->
receive
{tcp, Socket, Bin} ->
io:format("Bin:~p~n", [Bin]),
case binary_to_term(Bin) of
{rectangle, Width, Height} -> gen_tcp:send(Socket, term_to_binary(Width * Height));
{square, Side} -> gen_tcp:send(Socket, term_to_binary(Side * Side))
end,
loop(Socket);
{tcp_closed, _Socket} ->
io:format("server close ~n")
end.
如果是使用了{active,false},则使用recv的形式接收数据:
loop(Socket) ->
case gen_tcp:recv(Socket, 0) of
{ok, Bin} ->
io:format("Bin:~p~n", [Bin]),
case binary_to_term(Bin) of
{rectangle, Width, Height} -> gen_tcp:send(Socket, term_to_binary(Width * Height));
{square, Side} -> gen_tcp:send(Socket, term_to_binary(Side * Side))
end,
loop(Socket);
{error, closed} ->
io:format("server close ~n")
end.
gen_tcp:recv:从一个被动模式的套接字接受一个数据包
用法:
recv(Socket, Length) -> {ok, Packet} | {error, Reason}
这个函数是从一个被动模式的套接字接受一个数据包。如果返回一个 {error, closed} 的返回值,那表明 Socket 已经关闭
如果使用了{active, once}的形式,就必须显式调用inet:setopts才能重启下一个消息的接收,在此之前系统会处于阻塞状态
loop(Socket) ->
receive
{tcp, Socket, Bin} ->
io:format("Bin:~p~n", [Bin]),
case binary_to_term(Bin) of
{rectangle, Width, Height} -> gen_tcp:send(Socket, term_to_binary(Width * Height));
{square, Side} -> gen_tcp:send(Socket, term_to_binary(Side * Side))
end,
inet:setopts(Socket, [{active, once}]),
loop(Socket);
{tcp_close, Socket} ->
io:format("server close ~n")
end.
inet:peername
返回另一端连接的地址和端口
用法:
peername(Socket) -> {ok, {Address, Port}} | {error, posix()}
返回Socket对应的IP
{ok, {Ip, _Port}} = inet:peername(Socket),
inet:port
返回一个套接字的本地端口号
用法:
port(Socket) -> {ok, Port} | {error, any()}
案例
-module(socket_examples).
-compile(export_all).
-import(lists, [reverse/1]).
nano_get_url() ->
nano_get_url("www.google.com").
nano_get_url(Host) ->
{ok,Socket} = gen_tcp:connect(Host,80,[binary, {packet, 0}]), %% (1)
ok = gen_tcp:send(Socket, "GET / HTTP/1.0\r\n\r\n"), %% (2)
receive_data(Socket, []).
receive_data(Socket, SoFar) ->
receive
{tcp,Socket,Bin} -> %% (3)
receive_data(Socket, [Bin|SoFar]);
{tcp_closed,Socket} -> %% (4)
list_to_binary(reverse(SoFar)) %% (5)
end.
nano_client_eval(Str) ->
{ok, Socket} =
gen_tcp:connect("localhost", 2345,
[binary, {packet, 4}]),
ok = gen_tcp:send(Socket, term_to_binary(Str)),
receive
{tcp,Socket,Bin} ->
io:format("Client received binary = ~p~n",[Bin]),
Val = binary_to_term(Bin),
io:format("Client result = ~p~n",[Val]),
gen_tcp:close(Socket)
end.
start_nano_server() ->
{ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4}, %% (6)
{reuseaddr, true},
{active, true}]),
{ok, Socket} = gen_tcp:accept(Listen), %% (7)
gen_tcp:close(Listen), %% (8)
loop(Socket).
loop(Socket) ->
receive
{tcp, Socket, Bin} ->
io:format("Server received binary = ~p~n",[Bin]),
Str = binary_to_term(Bin), %% (9)
io:format("Server (unpacked) ~p~n",[Str]),
Reply = lib_misc:string2value(Str), %% (10)
io:format("Server replying = ~p~n",[Reply]),
gen_tcp:send(Socket, term_to_binary(Reply)), %% (11)
loop(Socket);
{tcp_closed, Socket} ->
io:format("Server socket closed~n")
end.
error_test() ->
spawn(fun() -> error_test_server() end),
lib_misc:sleep(2000),
{ok,Socket} = gen_tcp:connect("localhost",4321,[binary, {packet, 2}]),
io:format("connected to:~p~n",[Socket]),
gen_tcp:send(Socket, <<"123">>),
receive
Any ->
io:format("Any=~p~n",[Any])
end.
error_test_server() ->
{ok, Listen} = gen_tcp:listen(4321, [binary,{packet,2}]),
{ok, Socket} = gen_tcp:accept(Listen),
error_test_server_loop(Socket).
error_test_server_loop(Socket) ->
receive
{tcp, Socket, Data} ->
io:format("received:~p~n",[Data]),
atom_to_list(Data),
error_test_server_loop(Socket)
end.
创建某个套接字(通过调用gen_tcp:accept或gen_tcp:connect) 的进程被称为该套接字的控制进程。所有来自套接字的消息都会被发送到控制进程。如果控制进程挂了,套接字就会被关 闭 。
ETS和DETS
ets和dets是两个系统模块,可以用来高效存储海量的Erlang数据
ETS:可以用它存储海量的数据(只要有足够的内存),执行查找的时
间也是恒定的
ETS或DETS表其实就是Erlang元组的**集合 ,**ETS表没有垃圾收集机制
ETS表里的数据保存在内存里,它们是易失的。当ETS表被丢弃或者控制它的Erlang进程终止时,这些数据就会被删除。保存在DETS表里的数据是非易失的,即使整个系统崩溃也能留存下来。 DETS表在打开时会进行一致性检查,如果发现有损坏,系统就会尝试修复它
ETS和DETS表保存的是元组
一些表被称为异键表( set), 它们要求表里所有的键都是唯一的。另一些被称为同键表( bag),它们允许多个元素拥有相同的键
表类型 | 标识 | 说明 |
---|---|---|
异键表 | set | 它们要求表里所有的键都是唯一的 |
有序异键 | ordered set | 与set类似,会对元组进行排序 |
异键 | bag | 不同的元组可以有相同的键 |
副本同键 | duplicate bag | 可以有多个元组拥有相同的键 |
ETS常用API
ets:new/2 创建一个 ets 表
用法:
ets:new(Name, Options) -> tid() | atom()
Name -> atom, ets表名称
Options -> [] ,可以写多个参数
Options:Type | Access | named_table | {keypos, Pos} | {heir, Pid :: pid(), HeirData} | {heir, none} |
named_table:标识了该参数,就能够直接使用Name来操作ets表
keypos是步长,integer() >= 1,用于指定元组第几位,常用在使用Record的时候
Type: set、ordered set、bag、duplicate bag
Access: 访问权限,分为public(任何进程可读写),private(除了创建进程,其他进程不可以读写),protected(默认)(主进程可读写,其他进程只读不可写)
举例:
%% 创建一个名叫test的set表,并且要使用test作为后续操作
TableId = ets:new(test,[set,named_table,public]).
ets:insert/2 向 ETS 表插入数据
用法:
ets:insert(Tab, ObjectOrObjects) -> true
- 如果是 set 类型的表,并且在表里有跟插入的对象数据有相同的键,那么旧的对象数据将会被替换。
- 如果是一个bag表,一个键可以有多个不同的值
- 如果是一个duplicate bag,不同的值可以有相同的键
代码:
%% 在名为test的表上插入{a,1}数据
ets:insert(test, {a, 1}).
%% 批量插入
ets:insert(test,[{a,1},{b,2}]).
ets:lookup/2 在 ETS 表里查出相应键的值
用法:
ets:lookup(Tab, Key) -> [Object].
代码:
%% 查找test表中键为a的值
3> ets:lookup(test,a).
[{a,1}]
注意set返回的是一个[ {Key,Value} ]的形式,空则返回[]
而bag或duplicate_bag 则返回任意长度的列表
ets:lookup_element/3 返回 ETS 表里指定键的对象数据的第几个元素数据
用法:
ets:lookup_element(Tab, Key, Pos) -> Elem
Pos是步长
代码:
1> ets:new(test,[set,named_table]).
test
2> ets:insert(test,{a,1}).
true
3> ets:lookup_element(test,a,2).
1
注意,返回的是一个元素Elem
如果表里没有键为 Key 的对象,函数将以 badarg 的原因退出。
此时在代码中,可以使用 catch 的形式来捕获 没有Key的情况
case (catch ets:lookup_element(State, Ip, 2)) of
%% 如果为空就直接插入数据
{'EXIT', _} -> do_sthome...
_Other ->do....
end.
ets:insert_new/2 向 ETS 表插入新数据
用法:
insert_new(Tab, ObjectOrObjects) -> boolean()
这个函数跟 ets:insert/2 很相似,都是向 ETS 表 Tab 插入数据 ObjectOrObjects,不同的是,在插入数据时有相同的键要被覆盖替换(类型是 set 或 ordered_set 的表),或者要添加的多个对象数据的键已在表里存在(类型是 bag 或 duplicate_bag 的表),该函数则返回false。
如果插入的对象数据 ObjectOrObjects 是一个列表,那么在插入数据之前,将检测列表里的每一个键。如果列表里至少有一个键在表中已存在,则不会插入任何对象数据。
代码:
1> ets:new(test,[set,named_table]).
test
2> ets:insert(test,{a,1}).
true
3> ets:insert_new(test,{a,1}).
false
4> ets:insert_new(test,{b,1}).
true