UP | HOME

杂项

Table of Contents

大型程序通常会把代码拆分成一组文件,各个文件之间通过良好定义的接口来连接

拆分源码

现在将前面几节中 messager 程序分布到多个文件中

服务器配置头文件

%%%----FILE mess_config.hrl----

%%% Configure the location of the server node,
-define(server_node, messenger@gentoo).

%%%----END FILE----

客户端与 messager 之间的接口定义

%%%----FILE mess_interface.hrl----

%%%Message interface between client and server and client shell for
%%% messenger program 

%%%Messages from Client to server received in server/1 function.
-record(logon,{client_pid, username}).
-record(message,{client_pid, to_name, message}).
%%% {'EXIT', ClientPid, Reason}  (client terminated or unreachable.

%%% Messages from Server to Client, received in await_result/0 function 
-record(abort_client,{message}).
%%% Messages are: user_exists_at_other_node, 
%%%               you_are_not_logged_on
-record(server_reply,{message}).
%%% Messages are: logged_on
%%%               receiver_not_found
%%%               sent  (Message has been sent (no guarantee)
%%% Messages from Server to Client received in client/1 function
-record(message_from,{from_name, message}).

%%% Messages from shell to Client received in client/1 function
%%% spawn(mess_client, client, [server_node(), Name])
-record(message_to,{to_name, message}).
%%% logoff

%%%----END FILE----

用户接口函数

%%%----FILE user_interface.erl----

%%% User interface to the messenger program
%%% login(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.

-module(user_interface).
-export([logon/1, logoff/0, message/2]).
-include("mess_interface.hrl").
-include("mess_config.hrl").

logon(Name) ->
    case whereis(mess_client) of 
        undefined ->
            register(mess_client, 
                     spawn(mess_client, 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{to_name=ToName, message=Message},
             ok
    end.
%%%----END FILE----

messager 系统客户端的函数

%%%----FILE mess_client.erl----

%%% The client process which runs on each user node

-module(mess_client).
-export([client/2]).
-include("mess_interface.hrl").

client(Server_Node, Name) ->
    {messenger, Server_Node} ! #logon{client_pid=self(), username=Name},
    await_result(),
    client(Server_Node).

client(Server_Node) ->
    receive
        logoff ->
            exit(normal);
        #message_to{to_name=ToName, message=Message} ->
            {messenger, Server_Node} ! 
                #message{client_pid=self(), to_name=ToName, message=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
        #abort_client{message=Why} ->
            io:format("~p~n", [Why]),
            exit(normal);
        #server_reply{message=What} ->
            io:format("~p~n", [What])
    after 5000 ->
            io:format("No response from server~n", []),
            exit(timeout)
    end.

%%%----END FILE---

messager 服务端的函数

%%%----FILE mess_server.erl----

%%% This is the server process of the messenger service

-module(mess_server).
-export([start_server/0, server/0]).
-include("mess_interface.hrl").

server() ->
    process_flag(trap_exit, true),
    server([]).

%%% the user list has the format [{ClientPid1, Name1},{ClientPid22, Name2},...]
server(User_List) ->
    io:format("User list = ~p~n", [User_List]),
    receive
        #logon{client_pid=From, username=Name} ->
            New_User_List = server_logon(From, Name, User_List),
            server(New_User_List);
        {'EXIT', From, _} ->
            New_User_List = server_logoff(From, User_List),
            server(New_User_List);
        #message{client_pid=From, to_name=To, message=Message} ->
            server_transfer(From, To, Message, User_List),
            server(User_List)
    end.

%%% Start the server
start_server() ->
    register(messenger, spawn(?MODULE, 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 ! #abort_client{message=user_exists_at_other_node},
            User_List;
        false ->
            From ! #server_reply{message=logged_on},
            link(From),
            [{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 ! #abort_client{message=you_are_not_logged_on};
        {value, {_, 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 ! #server_reply{message=receiver_not_found};
        {value, {ToPid, To}} ->
            ToPid ! #message_from{from_name=Name, message=Message}, 
            From !  #server_reply{message=sent} 
    end.

%%%----END FILE---

测试

在服务器节点messenger@gentoo编译并启动服务器:

$ erl -sname messenger

Erlang/OTP 23 [erts-11.1.5] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

Eshell V11.1.5  (abort with ^G)
(messenger@gentoo)1> c (mess_server) .
{ok,mess_server}
(messenger@gentoo)2>
(messenger@gentoo)2> mess_server:start_server() .
User list = []
true

编译客户端后,peter在c1@raspberrypi登录:

$ erl -sname c1

Erlang/OTP 21 [erts-10.2.4] [source] [smp:4:4] [ds:4:4:10] [async-threads:1]

Eshell V10.2.4  (abort with ^G)
(c1@raspberrypi)1> c (user_interface) .
{ok,user_interface}
(c1@raspberrypi)2>
(c1@raspberrypi)2> user_interface:logon(peter) .
true
(c1@raspberrypi)3>
(c1@raspberrypi)3> =ERROR REPORT==== 18-Feb-2021::12:44:02.314766 ===
Error in process <0.92.0> on node c1@raspberrypi with exit value:
{undef,[{mess_client,client,[messenger@gentoo,peter],[]}]}


(c1@raspberrypi)3> c (mess_client) .
{ok,mess_client}
(c1@raspberrypi)4>
(c1@raspberrypi)4> user_interface:logon(peter) .
true
(c1@raspberrypi)5>
logged_on
注意:这里不仅要编译 user_interface.erl,还需要编译 mess_client.erl

否则就会报错:

=ERROR REPORT==== 18-Feb-2021::12:44:02.314766 ===
  Error in process <0.92.0> on node c1@raspberrypi with exit value:
  {undef,[{mess_client,client,[messenger@gentoo,peter],[]}]}

peter登录后,服务器端也会打印出新的用户列表:

(messenger@gentoo)3>
User list = [{<12948.100.0>,peter}] 

fred在节点 c2@gentoo 登录:

erl -sname c2
Erlang/OTP 23 [erts-11.1.5] [source] [64-bit] [smp:4:4] [ds:4:4:10] [async-threads:1] [hipe]

Eshell V11.1.5  (abort with ^G)
(c2@gentoo)1> c (user_interface) .
{ok,user_interface}
(c2@gentoo)2>
(c2@gentoo)2> c (mess_client) .
{ok,mess_client}
(c2@gentoo)3>
(c2@gentoo)3> user_interface:logon(fred) .
true
(c2@gentoo)4>
logged_on

peter登录后,服务器端也会再次打印出用户列表:

User list = [{<12950.102.0>,fred},{<12948.100.0>,peter}]

peter 尝试向 fred 发送消息:

(c1@raspberrypi)5> user_interface:message(fred, 'hello') .
ok
(c1@raspberrypi)6>
sent

fred 收到消息后,shell 会打印出:

Message from peter: hello

同样的服务器在转发消息的时候会再次打印用户列表:

User list = [{<12950.102.0>,fred},{<12948.100.0>,peter}]

基本测试到此已经成功

除了完成上述工作外:
1. 编写了头文件
2. 使用记录重新定义了 shell 、客户端以及服务端的消息格式
3. 引入了宏

下面会具体解释这些新知识

头文件

如上所示,某些文件的扩展名为 .hrl 。这些是在 .erl 文件中会用到的 头文件 ,使用方法如下:

-include("File_Name").
-include("mess_interface.hrl").
在本例中,上面所有的文件与 messager 系统的其它文件在同一个目录下

头文件中可以包含任何合法的 Erlang 代码,但是通常里面只包含一些 记录 的定义

记录

记录的定义如下:

-record(name_of_record,{field_name1, field_name2, field_name3, ......}).

例如:

-record(message_to,{to_name, message}).

这等价于:

{message_to, To_Name, Message}

用一个例子来说明怎样创建一个记录:

3> rd(message_to,{to_name, message}).    
message_to
4>  #message_to{message="hello", to_name=fred} . 
#message_to{to_name = fred,message = "hello"}

上面的代码创建了如下的记录:

{message_to, fred, "hello"}
使用这种方式创建记录时,不需要考虑给每个部分赋值时的顺序问题

把记录定义在头文件中的一个好处在于 修改接口会变得非常容易

例如,如果想在记录中添加一个新的域,只需要在使用该新域的地方进行修改就可以了,而不需要在每个使用记录的地方都进行修改

如果你在创建记录时漏掉了其中的某些域,则这些域会得到一个默认的原子值 undefined 

使用记录进行 模式匹配创建记录 是一样。例如,在 receive 的 case 中:

#message_to{to_name=ToName, message=Message} ->

这与下面的代码是一样的:

{message_to, ToName, Message}

在 messager 系统添加的另外一种东西是宏

在mess_config.hrl文件中包含如下的定义:

%%% Configure the location of the server node,
-define(server_node, messenger@super).

这个头文件被包括到了mess_server.erl文件中:

-include("mess_config.hrl").
这样在 mess_server.erl 中出现的每个 server_node 都被替换为 messenger@super

宏还被用于生成服务端进程:

spawn(?MODULE, server, [])
这是一个标准宏,也就是说,这是一个系统定义的宏而不是用户自定义的宏

?MODULE 宏总是被替换为当前模块名(也就是在文件开始的部分的 -module 定义的名称)

宏有许多的高级用法,作为参数只是其中之一

编译和链接

Messager 系统中的三个 Erlang .erl 文件被分别编译成三个独立的目标代码文件 .beam 中。当执行过程中引用到这些代码时,Erlang 系统会将它们 加载链接 到系统里

在本例中,把它们全部放到当前工作目录下,实际上也可以将这些文件放到其它目录下

在这个 messager 例子中,没有对发送消息的内容做出任何假设和限制,这些消息可以是任何合法的 Erlang 项 

Previous:容错编程

Home:目录