鍍金池/ 教程/ 區(qū)塊鏈/ 完整示例
注冊(cè)進(jìn)程名稱
錯(cuò)誤處理
完整示例
分布式編程
消息傳遞
if 與 case
健壯性
映射 (Map)
高階函數(shù) (Fun)
輸出至終端
更多關(guān)于列表的內(nèi)容
內(nèi)置函數(shù) (BIF)
模塊與函數(shù)
將大程序分在多個(gè)文件中
匹配、Guards 與變量的作用域
超時(shí)
列表
完整示例
頭文件
標(biāo)準(zhǔn)模塊與使用手冊(cè)
進(jìn)程
記錄
增加健壯性后的完整示例
Erlang Shell
原子類型

完整示例

接下來(lái)這個(gè)示例是一個(gè)簡(jiǎn)單的消息傳遞者(messager)示例。Messager 是一個(gè)允許用登錄到不同的結(jié)點(diǎn)并向彼此發(fā)送消息的應(yīng)用程序。

開始之前,請(qǐng)注意以下幾點(diǎn):

  • 這個(gè)示例只演示了消息傳遞的邏輯---沒有提供用戶友好的界面(雖然這在 Erlang 是可以做到的)。
  • 這類的問(wèn)題使用 OTP 的工具可以非常方便的實(shí)現(xiàn),還能同時(shí)提供線上更新的方法等。(參考 OTP 設(shè)計(jì)原則
  • 這個(gè)示例程序并不完整,它沒有考慮到結(jié)點(diǎn)離開等情況。這個(gè)問(wèn)題在后面的版本會(huì)得到修復(fù)。

Messager 允許 “客戶端” 連接到集中的服務(wù)器并表明其身份。也就是說(shuō),用戶并不需要知道另外一個(gè)用戶所在 Erlang 結(jié)點(diǎn)的名稱就可以發(fā)送消息。

messager.erl 文件內(nèi)容如下:

%%% Message passing utility.  
%%% User interface:
%%% logon(Name)
%%%     One user at a time can log in from each Erlang node in the
%%%     system messenger: and choose a suitable Name. If the Name
%%%     is already logged in at another node or if someone else is
%%%     already logged in at the same node, login will be rejected
%%%     with a suitable error message.
%%% logoff()
%%%     Logs off anybody at that node
%%% message(ToName, Message)
%%%     sends Message to ToName. Error messages if the user of this 
%%%     function is not logged on or if ToName is not logged on at
%%%     any node.
%%%
%%% One node in the network of Erlang nodes runs a server which maintains
%%% data about the logged on users. The server is registered as "messenger"
%%% Each node where there is a user logged on runs a client process registered
%%% as "mess_client" 
%%%
%%% Protocol between the client processes and the server
%%% ----------------------------------------------------
%%% 
%%% To server: {ClientPid, logon, UserName}
%%% Reply {messenger, stop, user_exists_at_other_node} stops the client
%%% Reply {messenger, logged_on} logon was successful
%%%
%%% To server: {ClientPid, logoff}
%%% Reply: {messenger, logged_off}
%%%
%%% To server: {ClientPid, logoff}
%%% Reply: no reply
%%%
%%% To server: {ClientPid, message_to, ToName, Message} send a message
%%% Reply: {messenger, stop, you_are_not_logged_on} stops the client
%%% Reply: {messenger, receiver_not_found} no user with this name logged on
%%% Reply: {messenger, sent} Message has been sent (but no guarantee)
%%%
%%% To client: {message_from, Name, Message},
%%%
%%% Protocol between the "commands" and the client
%%% ----------------------------------------------
%%%
%%% Started: messenger:client(Server_Node, Name)
%%% To client: logoff
%%% To client: {message_to, ToName, Message}
%%%
%%% Configuration: change the server_node() function to return the
%%% name of the node where the messenger server runs

-module(messenger).
-export([start_server/0, server/1, logon/1, logoff/0, message/2, client/2]).

%%% Change the function below to return the name of the node where the
%%% messenger server runs
server_node() ->
    messenger@bill.

%%% This is the server process for the "messenger"
%%% the user list has the format [{ClientPid1, Name1},{ClientPid22, Name2},...]
server(User_List) ->
    receive
        {From, logon, Name} ->
            New_User_List = server_logon(From, Name, User_List),
            server(New_User_List);
        {From, logoff} ->
            New_User_List = server_logoff(From, User_List),
            server(New_User_List);
        {From, message_to, To, Message} ->
            server_transfer(From, To, Message, User_List),
            io:format("list is now: ~p~n", [User_List]),
            server(User_List)
    end.

%%% Start the server
start_server() ->
    register(messenger, spawn(messenger, server, [[]])).

%%% Server adds a new user to the user list
server_logon(From, Name, User_List) ->
    %% check if logged on anywhere else
    case lists:keymember(Name, 2, User_List) of
        true ->
            From ! {messenger, stop, user_exists_at_other_node},  %reject logon
            User_List;
        false ->
            From ! {messenger, logged_on},
            [{From, Name} | User_List]        %add user to the list
    end.

%%% Server deletes a user from the user list
server_logoff(From, User_List) ->
    lists:keydelete(From, 1, User_List).

%%% Server transfers a message between user
server_transfer(From, To, Message, User_List) ->
    %% check that the user is logged on and who he is
    case lists:keysearch(From, 1, User_List) of
        false ->
            From ! {messenger, stop, you_are_not_logged_on};
        {value, {From, Name}} ->
            server_transfer(From, Name, To, Message, User_List)
    end.
%%% If the user exists, send the message
server_transfer(From, Name, To, Message, User_List) ->
    %% Find the receiver and send the message
    case lists:keysearch(To, 2, User_List) of
        false ->
            From ! {messenger, receiver_not_found};
        {value, {ToPid, To}} ->
            ToPid ! {message_from, Name, Message}, 
            From ! {messenger, sent} 
    end.

%%% User Commands
logon(Name) ->
    case whereis(mess_client) of 
        undefined ->
            register(mess_client, 
                     spawn(messenger, client, [server_node(), Name]));
        _ -> already_logged_on
    end.

logoff() ->
    mess_client ! logoff.

message(ToName, Message) ->
    case whereis(mess_client) of % Test if the client is running
        undefined ->
            not_logged_on;
        _ -> mess_client ! {message_to, ToName, Message},
             ok
end.

%%% The client process which runs on each server node
client(Server_Node, Name) ->
    {messenger, Server_Node} ! {self(), logon, Name},
    await_result(),
    client(Server_Node).

client(Server_Node) ->
    receive
        logoff ->
            {messenger, Server_Node} ! {self(), logoff},
            exit(normal);
        {message_to, ToName, Message} ->
            {messenger, Server_Node} ! {self(), message_to, ToName, Message},
            await_result();
        {message_from, FromName, Message} ->
            io:format("Message from ~p: ~p~n", [FromName, Message])
    end,
    client(Server_Node).

%%% wait for a response from the server
await_result() ->
    receive
        {messenger, stop, Why} -> % Stop the client 
            io:format("~p~n", [Why]),
            exit(normal);
        {messenger, What} ->  % Normal response
            io:format("~p~n", [What])
    end.

在使用本示例程序之前,你需要:

  • 配置 server_node() 函數(shù)。
  • 將編譯后的代碼(messager.beam)拷貝到每一個(gè)你啟動(dòng)了 Erlang 的計(jì)算機(jī)上。

這接下來(lái)的例子中,我們?cè)谒呐_(tái)不同的計(jì)算上啟動(dòng)了 Erlang 結(jié)點(diǎn)。如果你的網(wǎng)絡(luò)沒有那么多的計(jì)算機(jī),你也可以在同一臺(tái)計(jì)算機(jī)上啟動(dòng)多個(gè)結(jié)點(diǎn)。

啟動(dòng)的四個(gè)結(jié)點(diǎn)分別為:messager@super,c1@bilo,c2@kosken,c3@gollum。

首先在 meesager@super 上啟動(dòng)服務(wù)器程序:

(messenger@super)1> messenger:start_server().
true

接下來(lái)用 peter 是在 c1@bibo 登錄:

(c1@bilbo)1> messenger:logon(peter).
true
logged_on

然后 James 在 c2@kosken 上登錄:

(c2@kosken)1> messenger:logon(james).
true
logged_on

最后,用 Fred 在 c3@gollum 上登錄:

(c3@gollum)1> messenger:logon(fred).
true
logged_on

現(xiàn)在,Peter 就可以向 Fred 發(fā)送消息了:

(c1@bilbo)2> messenger:message(fred, "hello").
ok
sent

Fred 收到消息后,回復(fù)一個(gè)消息給 Peter 然后登出:

Message from peter: "hello"
(c3@gollum)2> messenger:message(peter, "go away, I'm busy").
ok
sent
(c3@gollum)3> messenger:logoff().
logoff

隨后,James 再向 Fred 發(fā)送消息時(shí),則出現(xiàn)下面的情況:

(c2@kosken)2> messenger:message(fred, "peter doesn't like you").
ok
receiver_not_found

因?yàn)?Fred 已經(jīng)離開,所以發(fā)送消息失敗。

讓我們先來(lái)看看這個(gè)例子引入的一些新的概念。

這里有兩個(gè)版本的 server_transfer 函數(shù):其中一個(gè)有四個(gè)參數(shù)(server_transfer/4)另外一個(gè)有五個(gè)參數(shù)(server_transfer/5)。Erlang 將它們看作兩個(gè)完全不一樣的函數(shù)。

請(qǐng)注意這里是如何讓 server_transfer 函數(shù)通過(guò) server(User_List) 調(diào)用其自身的,這里形成了一個(gè)循環(huán)。 Erlang 編譯器非常的聰明,它會(huì)將上面的代碼優(yōu)化為一個(gè)循環(huán)而不是一個(gè)非法的遞規(guī)函數(shù)調(diào)用。但是它只能是在函數(shù)調(diào)用后面沒有別的代碼的情況下才能工作(注:即尾遞規(guī))。

示例中用到了lists 模塊中的函數(shù)。lists 模塊是一個(gè)非常有用的模塊,推薦你通過(guò)用戶手冊(cè)仔細(xì)研究一下(erl -man lists)。

lists:keymemeber(Key,Position,Lists) 函數(shù)遍歷列表中的元組,查看每個(gè)元組的指定位置 (Position)處的數(shù)據(jù)并判斷元組該位置是否與 Key 相等。元組中的第一個(gè)元素的位置為 1,依次類推。如果發(fā)現(xiàn)某個(gè)元組的 Position 位置處的元素與 Key 相同,則返回 true,否則返回 false。

3> lists:keymember(a, 2, [{x,y,z},{b,b,b},{b,a,c},{q,r,s}]).
true
4> lists:keymember(p, 2, [{x,y,z},{b,b,b},{b,a,c},{q,r,s}]).
false

lists:keydelete 與 lists:keymember 非常相似,只不過(guò)它將刪除列表中找到的第一個(gè)元組(如果存在),并返回剩余的列表:

5> lists:keydelete(a, 2, [{x,y,z},{b,b,b},{b,a,c},{q,r,s}]).
[{x,y,z},{b,b,b},{q,r,s}]

lists:keysearch 與 lists:keymember 類似,但是它將返回 {value,Tuple_Found} 或者原子值 false。

lists 模塊中還有許多非常有用的函數(shù)。

Erlang 進(jìn)程(概念上地)會(huì)一直運(yùn)行直到它執(zhí)行 receive 命令,而此時(shí)消息隊(duì)列中又沒有它想接收的消息為止。
這兒,“概念上地” 是因?yàn)?Erlang 系統(tǒng)活躍的進(jìn)程實(shí)際上是共享 CPU 處理時(shí)間的。

當(dāng)進(jìn)程無(wú)事可做時(shí),即一個(gè)函數(shù)調(diào)用 return 返回而沒有調(diào)用另外一個(gè)函數(shù)時(shí),進(jìn)程就結(jié)束。另外一種終止進(jìn)程的方式是調(diào)用 exit/1 函數(shù)。exit/1 函數(shù)的參數(shù)是有特殊含義的,我們稍后會(huì)討論到。在這個(gè)例子中使用 exit(normal) 結(jié)束進(jìn)程,它與程序因沒有再調(diào)用函數(shù)而終止的效果是一樣的。

內(nèi)置函數(shù) whereis(RegisteredName) 用于檢查是否已有一個(gè)進(jìn)程注冊(cè)了進(jìn)程名稱 RegisteredName。如果已經(jīng)存在,則返回進(jìn)程 的進(jìn)程標(biāo)識(shí)符。如果不存在,則返回原子值 undefined。

到這兒,你應(yīng)該已經(jīng)可以看懂 messager 模塊的大部分代碼了。讓我們來(lái)深入研究將一個(gè)消息從一個(gè)用戶發(fā)送到另外一個(gè)的詳細(xì)過(guò)程。

當(dāng)?shù)谝粋€(gè)用戶調(diào)用 “sends” 發(fā)送消息時(shí):

messenger:message(fred, "hello")

首先檢查用戶自身是否在系統(tǒng)中運(yùn)行(是否可以查找到 mess_client 進(jìn)程):

whereis(mess_client) 

如果用戶存在則將消息發(fā)送給 mess_client:

mess_client ! {message_to, fred, "hello"}

客戶端通過(guò)下面的代碼將消息發(fā)送到服務(wù)器:

{messenger, messenger@super} ! {self(), message_to, fred, "hello"},

然后等待服務(wù)器的回復(fù)。 服務(wù)器收到消息后將調(diào)用:

{messenger, messenger@super} ! {self(), message_to, fred, "hello"},

接下來(lái),用下面的代碼檢查進(jìn)程標(biāo)識(shí)符 From 是否在 User_Lists 列表中:

lists:keysearch(From, 1, User_List)

如果 keysearch 返回原子值 false,則出現(xiàn)的某種錯(cuò)誤,服務(wù)將返回如下消息:

From ! {messenger, stop, you_are_not_logged_on}

client 收到這個(gè)消息后,則執(zhí)行 exit(normal) 然后終止程序。如果 keysearch 返回的是 {value,{From,Nmae}} ,則可以確定該用戶已經(jīng)登錄,并其名字(peter)存儲(chǔ)在變量 Name 中。

接下來(lái)調(diào)用:

server_transfer(From, peter, fred, "hello", User_List)

注意這里是函數(shù) server_transfer/5,與其它的 server_transfer/4 不是同一個(gè)函數(shù)。還會(huì)再次調(diào)用 keysearch 函數(shù)用于在 User_List 中查找與 fred 對(duì)應(yīng)的進(jìn)程標(biāo)識(shí)符:

lists:keysearch(fred, 2, User_List)

這一次用到了參數(shù) 2,這表示是元組中的第二個(gè)元素。如果返回的是原子值 false,則說(shuō)明 fred 已經(jīng)登出,服務(wù)器將向發(fā)送消息的進(jìn)程發(fā)送如下消息:

From ! {messenger, receiver_not_found};

client 就會(huì)收到該消息。

如果 keysearch 返回值為:

{value, {ToPid, fred}}

則會(huì)將下面的消息發(fā)送給 fred 客戶端:

ToPid ! {message_from, peter, "hello"}, 

而如下的消息會(huì)發(fā)送給 peter 的客戶端:

From ! {messenger, sent} 

Fred 客戶端收到消息后將其輸出:

{message_from, peter, "hello"} ->
    io:format("Message from ~p: ~p~n", [peter, "hello"])

peter 客戶端在 await_result 函數(shù)中收到回復(fù)的消息。