《Erlang程序设计》附录D 套接字应用程序


附录D 套接字应用程序

Table of Contents

附录D 套接字应用程序

D.1 例子

首先通过一个例子来学习如何使用lib_chan。

D.1.1 第一步:写一个配置文件

# 指定端口号
{port, 2233}.

# 指定服务名、密码、模块名、函数名、参数 
{service, math, password, "qwerty", mfa, mod_math, run, []}.

此配置文件声明了一个名为math的服务, 会在2233端口提供服务, 密码为qwerty, 服务内容在mod_math中定义, 并通过mod_math:run/3来启动。

D.1.2 第二步:编写服务器代码

-module(mod_math).
-export([run/3]).

%% 开启处理进程
%% MM为中间人进程的PID, ArgC来自客户端, ArgS来自配置文件
run(MM, ArgC, ArgS) ->
    io:format("mod_math:run starting~nArgC = ~p ArgS = ~p~n", [ArgC, ArgS]),
    loop(MM).

%% 循环接收消息
%% 根据客户端提交的不同参数调用不同的函数并将结果作为消息发送给中间人进程 
loop(MM) ->
    receive
        {chan, MM, {factorial, N}} ->
            MM ! {send, fac(N)},
            loop(MM);
        {chan, MM, {fibonacci, N}} ->
            MM ! {send, fib(N)},
            loop(MM);
        {chan_closed, MM} ->
            io:format("mod_math stopping~n"),
            exit(normal)
    end.

%% 求阶乘的函数
fac(0) ->1;
fac(N) ->N*fac(N-1).

%% 求斐波那契数列的函数
fib(1) ->1;
fib(2) ->1;
fib(N) ->fib(N-1) + fib(N-2).

D.1.3 第三步:启动服务器

# 打开一个Erlang shell
1> lib_chan:start_server("./config").
lib_chan starting:"./config"
ConfigData=[{port,2233},{service,math,password,"qwerty",mfa,mod_math,run,[]}]
true

D.1.4 第四步:通过网络访问服务器

# 新开一个Erlang shell
1> {ok, S} = lib_chan:connect("localhost", 2233, math, "qwerty", {yes, go}).
{ok,<0.33.0>}

# 此时切换到服务端的shell, 可以看到如下输出
mod_math:run starting
ArgC = {yes,go} ArgS = []

# 切换到客户端的shell, 进行服务调用
2> lib_chan:rpc(S, {factorial, 20}).
2432902008176640000
3> lib_chan:rpc(S, {fibonacci, 15}).
610

# 可以正常得到结果, 关闭服务
4> lib_chan:disconnect(S).
close

# 切换到服务端的shell, 可以看到如下输出
mod_math stopping 

D.2 lib_chan如何工作

D.2.1 启动服务器

通过程序的调用方式来分析lib_chan的工作流程

# 打开一个Erlang shell
1> lib_chan:start_server("./config").
lib_chan starting:"./config"
ConfigData=[{port,2233},{service,math,password,"qwerty",mfa,mod_math,run,[]}]
true

首先来看lib_chan中的实现

%% 以默认方式启动服务
%% 获取系统变量值, 加载其目录下的配置文件
start_server() ->
    case os:getenv("HOME") of
    false ->
        exit({ebadEnv, "HOME"});
    Home ->
        start_server(Home ++ "/.erlang_config/lib_chan.conf")
    end.

%% 使用指定的配置文件启动服务
start_server(ConfigFile) ->
    io:format("lib_chan starting:~p~n",[ConfigFile]),
    %% 读取Erlang格式的配置项
    case file:consult(ConfigFile) of
    {ok, ConfigData} ->
        io:format("ConfigData=~p~n",[ConfigData]),
        %% 检测配置项
        case check_terms(ConfigData) of
            %% 没有错误项则通过start_server1启动服务
            []     ->start_server1(ConfigData);
            Errors ->exit({eDaemonConfig, Errors})
        end;
    {error, Why} ->
        exit({eDaemonConfig, Why})
    end.

%% 检测配置项
%% 使用map操作对每一个配置项调用check_term函数进行检测
%% 提取出现错误的项组成一个列表(即Errors)
check_terms(ConfigData) ->
    L = map(fun check_term/1, ConfigData),
    [X || {error, X} <- L].

%% check_term函数这里只检测两种格式的配置项
%% 端口号需为数值型
%% 需提供服务名、密码、模块名、函数名、参数列表         
check_term({port, P}) when is_integer(P)     ->ok;
check_term({service,_,password,_,mfa,_,_,_}) ->ok;
check_term(X) ->{error, {badTerm, X}}.

%% 使用经过检测后的配置项通过start_server2启动服务进程, 并将进程注册为lib_chan
start_server1(ConfigData) ->
    register(lib_chan, spawn(fun() ->start_server2(ConfigData) end)).

%% 从配置项中提取端口号, 并通过start_port_server启动服务
start_server2(ConfigData) ->
    [Port] = [ P || {port,P} <- ConfigData],
    start_port_server(Port, ConfigData).

%% 通过lib_chan_cs模块的start_raw_server函数启动服务进程
%% 设置服务接收到客户端请求后由start_port_instance函数进行处理
%% 同时设置最大连接数为100
%% 套接字数据包长度为4
%% 这个函数很关键, 因为它设置了在接收到连接请求后的处理方式
%% 即start_port_instance函数 
start_port_server(Port, ConfigData) ->
    lib_chan_cs:start_raw_server(Port, 
                fun(Socket) ->
                    start_port_instance(Socket, 
                                ConfigData) end,
                100,
                4).

然后来看lib_chan_cs中的实现

start_raw_server(Port, Fun, Max, PacketLength) ->
    %% 将端口号拼装成原子(portServer+port)作为服务的注册名
    %% 判断服务是否已注册
    %% 没有注册则调用cold_start启动服务
    %% 然后完成注册和错误处理
    Name = port_name(Port),
    case whereis(Name) of
        undefined ->
            Self = self(),
            Pid = spawn_link(fun() ->
                     cold_start(Self, Port, Fun, Max, PacketLength)
                     end),
            receive
                {Pid, ok} ->
                    register(Name, Pid),
                    {ok, self()};
                {Pid, Error} ->
                    Error
            end;
        _Pid ->
            {error, already_started}
    end.

%% 拼接服务的注册名
port_name(Port) when integer(Port) ->
    list_to_atom("portServer" ++ integer_to_list(Port)).

cold_start(Master, Port, Fun, Max, PacketLength) ->
    %% 启动监听器, 最多允许Max个连接, 数据包长度为PacketLength
    process_flag(trap_exit, true),
    io:format("Starting a port server on ~p...~n",[Port]),
    case gen_tcp:listen(Port, [binary,
                   %% {dontroute, true},
                   {nodelay,true},
                   {packet, PacketLength},
                   {reuseaddr, true}, 
                   {active, true}]) of
        {ok, Listen} ->
            %% 监听建立后通知Master进程
            %% 由start_accept函数处理侦听
            %% 通过socket_loop函数进行socket连接的管理
            io:format("Listening to:~p~n",[Listen]),
            Master ! {self(), ok},
            New = start_accept(Listen, Fun),
            %% Now we're ready to run
            socket_loop(Listen, New, [], Fun, Max);
        Error ->
            Master ! {self(), Error}
    end.

start_accept(Listen, Fun) ->
    %% 调用start_child处理侦听
    S = self(),
    spawn_link(fun() ->start_child(S, Listen, Fun) end).

start_child(Parent, Listen, Fun) ->
    %% 最终实际由gen_tcp模块的accept函数进行侦听
    case gen_tcp:accept(Listen) of
        {ok, Socket} ->
            %% 接收到连接请求后通知连接管理进程 
            Parent ! {istarted, self()},            % tell the controller
            inet:setopts(Socket, [{packet,4},
                      binary,
                      {nodelay,true},
                      {active, true}]), 
            %% before we activate socket
            %% io:format("running the child:~p Fun=~p~n", [Socket, Fun]),
            process_flag(trap_exit, true),
            %% 使用lib_chan模块中start_port_server函数调用
            %% lib_chan_cs模块的start_raw_server函数时设置的socket处理函数
            %% 来进行处理, 并根据不同结果进行相应的返回
            case (catch Fun(Socket)) of
                {'EXIT', normal} ->
                    true;
                {'EXIT', Why} ->
                    io:format("Port process dies with exit:~p~n",[Why]),
                    true;
                _ ->
                    %% not an exit so everything's ok
                    true
            end
    end.

socket_loop(Listen, New, Active, Fun, Max) ->
    %% 根据监听结果通过possibly_start_another判断是否继续启动监听
    receive
        %% 有一个新的连接请求建立后则扩充Active
        {istarted, New} ->
            Active1 = [New|Active],
            possibly_start_another(false, Listen, Active1, Fun, Max);
        {'EXIT', New, _Why} ->
            %% io:format("Child exit=~p~n",[Why]),
            possibly_start_another(false,Listen,Active,Fun,Max);
        %% 有连接退出则从Active中删除
        {'EXIT', Pid, _Why} ->
            %% io:format("Child exit=~p~n",[Why]),
            Active1 = lists:delete(Pid, Active),
            possibly_start_another(New,Listen,Active1,Fun,Max);
        %% 其它情况则递归调用socket_loop进行socket的管理
        {children, From} ->
            From ! {session_server, Active},
            socket_loop(Listen,New,Active,Fun,Max);
        _Other ->
            socket_loop(Listen,New,Active,Fun,Max)
    end.


possibly_start_another(New, Listen, Active, Fun, Max) 
    when pid(New) ->
        socket_loop(Listen, New, Active, Fun, Max);
possibly_start_another(false, Listen, Active, Fun, Max) ->
    %% 判断是否已经超过最大连接数量
    case length(Active) of
        N when N < Max ->
            New = start_accept(Listen, Fun),
            socket_loop(Listen, New, Active, Fun,Max);
        _ ->
            socket_loop(Listen, false, Active, Fun, Max)
    end.

至此已经通过lib_chan启动了服务, 并设置了连接请求建立后的处理逻辑, 同时还设置了对连接请求数量的管理.

D.2.2 连接服务器

# 新开一个Erlang shell
1> {ok, S} = lib_chan:connect("localhost", 2233, math, "qwerty", {yes, go}).
{ok,<0.33.0>}

# 此时切换到服务端的shell, 可以看到如下输出
mod_math:run starting
ArgC = {yes,go} ArgS = []

首先来看lib_chan中的实现

%% 通过指定的主机、端口、服务名、密码、参数进行连接
connect(Host, Port, Service, Secret, ArgC) ->
    S = self(),
    %% 启动中间人进程
    MM = spawn(fun() ->connect(S, Host, Port) end),
    receive
    {MM, ok} ->
        %% 中间人进程启动成功则进行相关验证
        case authenticate(MM, Service, Secret, ArgC) of
        ok    ->{ok, MM};
        Error ->Error
        end;
    {MM, Error} ->
        Error
    end.

connect(Parent, Host, Port) ->
    %% 通过lib_chan_cs模块的start_raw_client函数完成实际的连接过程
    case lib_chan_cs:start_raw_client(Host, Port, 4) of
    {ok, Socket} ->
        %% 连接成功则通过 ok 标示通知中间人进程进行验证
        %% 然后由中间人循环进行socket的处理
        Parent ! {self(), ok},
        lib_chan_mm:loop(Socket, Parent);
    Error ->
        Parent ! {self(),  Error}
    end.

再看lib_chan_cs中的实现

%% 调用gen_tcp模块的connect函数对指定主机、端口进行连接
start_raw_client(Host, Port, PacketLength) ->
    gen_tcp:connect(Host, Port,
            [binary, {active, true}, {packet, PacketLength}]).

然后返回lib_chan看验证机制的实现

%% 验证机制
authenticate(MM, Service, Secret, ArgC) ->
    %% 向中间人进程发送消息
    %% 消息为startService、服务名、参数
    send(MM, {startService, Service, ArgC}),
    %% we should get back a challenge or a ack or closed socket
    receive
    {chan, MM, ack} ->
        ok;
    %% 收到暗号, 计算生成反馈值
    {chan, MM, {challenge, C}} ->
        %% 生成过程参见lib_chan_auth模块源码
        R = lib_chan_auth:make_response(C, Secret),
        %% 将反馈值发送给中间人
        %% 参见do_authentication函数
        send(MM, {response, R}),
        receive
        {chan, MM, ack} ->
            ok;
        %% 验证失败则关闭连接
        {chan, MM, authFail} ->
            wait_close(MM),
            {error, authFail};
        Other ->
            {error, Other}
        end;
    {chan, MM, badService} ->
        wait_close(MM),
        {error, badService};
    Other ->
        {error, Other}
    end.

此时还要看服务启动时对连接请求的处理设置

%% 通过lib_chan_cs模块进行服务的管理和客户端链接
start_port_server(Port, ConfigData) ->
    lib_chan_cs:start_raw_server(Port, 
                fun(Socket) ->
                    start_port_instance(Socket, 
                                ConfigData) end,
                100,
                4).

%% 服务启动后, 接收到请求后创建controller进程, 并由中间人完成对传递信息的编解码
start_port_instance(Socket, ConfigData) ->
    %% This is where the low-level connection is handled
    %% We must become a middle man
    %% But first we spawn a connection handler
    S = self(),
    Controller = spawn_link(fun() ->start_erl_port_server(S, ConfigData) end),
    lib_chan_mm:loop(Socket, Controller).

%% 从配置信息中读取配置进行相关验证
start_erl_port_server(MM, ConfigData) ->
    receive
    %% 在这里接收到中间人启动成功进行验证的消息
    {chan, MM, {startService, Mod, ArgC}} ->
        %% 根据不同的服务配置项进行不同的处理
        case get_service_definition(Mod, ConfigData) of
        %% 有密码的服务
        {yes, Pwd, MFA} ->
            case Pwd of
            none ->
                %% 通知中间人没有密码, 直接启动
                send(MM, ack),
                really_start(MM, ArgC, MFA);
            %% 验证密码
            _ ->
                do_authentication(Pwd, MM, ArgC, MFA)
            end;
        no ->
            io:format("sending bad service~n"),
            send(MM, badService),
            close(MM)
        end;
    Any ->
        io:format("*** ErL port server got:~p ~p~n",[MM, Any]),
        exit({protocolViolation, Any})
    end.

%% 根据不同的配置项返回不同的标示
get_service_definition(Mod, [{service, Mod, password, Pwd, mfa, M, F, A}|_]) ->
    {yes, Pwd, {M, F, A}};
get_service_definition(Name, [_|T]) ->
    get_service_definition(Name, T);
get_service_definition(_, []) ->
    no.

%% 验证密码的过程
do_authentication(Pwd, MM, ArgC, MFA) ->
    %% 生产暗号, 生成过程参见lib_chan_auth模块源码 
    C = lib_chan_auth:make_challenge(),
    %% 将暗号发送给中间人
    send(MM, {challenge, C}),
    receive
    %% 收到authenticate函数生成的反馈后进行验证
    {chan, MM, {response, R}} ->
        case lib_chan_auth:is_response_correct(C, R, Pwd) of
        true ->
            %% 验证通过则启动功能调用
            send(MM, ack),
            really_start(MM, ArgC, MFA);
        false ->
            send(MM, authFail),
            close(MM)
        end
    end.

%% 这里实际就是使用apply调用指定模块的指定函数
really_start(MM, ArgC, {Mod, Func, ArgS}) ->
    %% authentication worked so now we're off
    case (catch apply(Mod,Func,[MM,ArgC,ArgS])) of
    {'EXIT', normal} ->
        true;
    {'EXIT', Why} ->
        io:format("server error:~p~n",[Why]);
    Why ->
        io:format("server error should die with exit(normal) was:~p~n",
              [Why])
    end.

最后看mod_math的具体实现

%% 启动loop循环等待通过中间人发送的数据 
run(MM, ArgC, ArgS) ->
    io:format("mod_math:run starting~nArgC = ~p ArgS = ~p~n", [ArgC, ArgS]),
    loop(MM).

D.2.3 调用服务

3> lib_chan:rpc(S, {fibonacci, 15}).
610

首先来看lib_chan中的实现

%% 通过中间人的send方法发送数据
%% 接收到返回的数据后直接输出
rpc(MM, Q) ->
    send(MM, Q),
    receive
    {chan, MM, Reply} ->
        Reply
    end.

再看lib_chan_mm中的实现

%% 实际就是个简单的向指定进程发送数据的过程
send(Pid, Term)       ->Pid ! {send, Term}.

%% 中间人的loop循环处理数据 
loop1(Socket, Pid, Trace) ->
    receive
    ...
    %% 这里处理数据发送 
    {send, Term}  ->
        trace_it(Trace, {sendingMessage, Term}),
        %% 使用term_to_binary函数进行数据转换, 最终还是调用gen_tcp模块进行发送 
        gen_tcp:send(Socket, term_to_binary(Term)),
        loop1(Socket, Pid, Trace);
    ...
    end.

发送后的数据实际还是由中间人来处理

loop1(Socket, Pid, Trace) ->
    receive
    {tcp, Socket, Bin} ->
        %% 这里使用binary_to_term完成数据转换 
        Term = binary_to_term(Bin),
        trace_it(Trace,{socketReceived, Term}),
        %% 以chan作为标示向指定进程发送数据
        Pid ! {chan, self(), Term},
        loop1(Socket, Pid, Trace);
    ...

最后看mod_math的具体实现

loop(MM) ->
    receive
        %% 模式匹配, 根据不同参数进行不同的计算
        %% 将计算结果发送给中间人, 继续等待 
        {chan, MM, {factorial, N}} ->
            MM ! {send, fac(N)},
            loop(MM);
        {chan, MM, {fibonacci, N}} ->
            MM ! {send, fib(N)},
            loop(MM);
        {chan_closed, MM} ->
            io:format("mod_math stopping~n"),
            exit(normal)
    end.
fac(0) ->1;
fac(N) ->N*fac(N-1).

fib(1) ->1;
fib(2) ->1;
fib(N) ->fib(N-1) + fib(N-2).

数据发送给中间人后又会走到中间人循环处理过程的发送模式

{send, Term} -> ...

然后又会走到中间人循环处理过程中的数据解析模式

{tcp, Socket, Bin} -> ...

最后将最终结果直接输出

rpc(MM, Q) ->
    send(MM, Q),
    receive
    %% 接收到数据后直接输出 
    {chan, MM, Reply} ->
        Reply
    end.

D.2.4 关闭服务

4> lib_chan:disconnect(S).
close

首先来看lib_chan中的实现

disconnect(MM) -> close(MM).

然后来看lib_chan_mm中的实现

close(Pid)  -> Pid ! close.

%% 在loop1循环中处理关闭操作
loop1(Socket, Pid, Trace) ->
    receive
    ...
    close ->
        trace_it(Trace, closedByClient),
        %% 即实际上最终调用gen_tcp模块完成套接字的关闭操作
        gen_tcp:close(Socket);
    ...
    end.

Date: 2013-08-19 15:37:32 CST

Author: matrix

Org version 7.8.11 with Emacs version 24

Validate XHTML 1.0

长按二维码向我转账

受苹果公司新规定影响,微信 iOS 版的赞赏功能被关闭,可通过二维码转账支持公众号。

    阅读
    好看
    已推荐到看一看
    你的朋友可以在“发现”-“看一看”看到你认为好看的文章。
    已取消,“好看”想法已同步删除
    已推荐到看一看 和朋友分享想法
    最多200字,当前共 发送

    已发送

    朋友将在看一看看到

    确定
    分享你的想法...
    取消

    分享想法到看一看

    确定
    最多200字,当前共

    发送中

    网络异常,请稍后重试

    微信扫一扫
    关注该公众号





    联系我们

    欢迎来到TinyMind。

    关于TinyMind的内容或商务合作、网站建议,举报不良信息等均可联系我们。

    TinyMind客服邮箱:support@tinymind.net.cn