zoukankan      html  css  js  c++  java
  • 《Erlang程序设计》第十四章 套接字编程

    第十四章 套接字编程

    第十四章 套接字编程

    14.1 使用TCP

    14.1.1 从服务器上获取数据

    -module(socket_examples).
    -export([nano_get_url/0]).
    -import(lists, [reverse/1]).
    
    nano_get_url() ->
        nano_get_url("www.google.com").
    nano_get_url(Host) ->
        %% 链接到主机的80端口, 以二进制模式打开套接字, 原始方式发送TCP数据
        {ok, Socket} = gen_tcp:connect(Host, 80, [binary, {packet, 0}]),
        %% 发送GET消息到套接字, 使用reverse_data接收数据
        ok = gen_tcp:send(Socket, "GET / HTTP/1.0
    
    "),
        receive_data(Socket, []).
    
    receive_data(Socket, SoFar) ->
        %% 回应消息一帧一帧的返回, 因此这里使用receive方式接收
        receive
            {tcp, Socket, Bin} ->
                %% 将接收到的数据添加到列表SoFar中
                receive_data(Socket, [Bin|SoFar]);
            {tcp_closed, Socket} ->
                %% 因为每接收一帧数据都是放在SoFar的头部, 因此接收完成后需要翻转列表得到正常顺序的数据
                list_to_binary(reverse(SoFar))
        end.
    

      运行结果:

    1> socket_examples:nano_get_url().
    <<"HTTP/1.0 200 OK
    Date: Mon, 04 Nov 2013 02:32:00 GMT
    Expires: -1
    Cache-Control: private, max-age=0
    Content-Type: "...>>
    

    14.1.2 一个简单的TCP服务器

      服务端:

    start_nano_server() ->
        %% 监听来自端口2345的链接, 设置包规则为带有4字节长的包头
        {ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4}, {reuseaddr, true}, {active, true}]),
        %% 只处理正常打开的套接字
        {ok, Socket} = gen_tcp:accept(Listen),
        %% 只处理一个链接
        gen_tcp:close(Listen),
        %% 链接处理
        loop(Socket).
    
    loop(Socket) ->
        receive
            {tcp, Socket, Bin} ->
                %% 输出二进制数据
                io:format("Server received binary = ~p~n", [Bin]),
                %% 格式转换
                Str = binary_to_term(Bin),
                io:format("Server (unpacked) ~p~n", [Str]),
                %% 对字符串求值
                Reply = string2value(Str),
                io:format("Server replying = ~p~n", [Reply]),
                %% 对结果编码后发给套接字
                gen_tcp:send(Socket, term_to_binary(Reply)),
                loop(Socket);
            {tcp_closed, Socket} ->
                io:format("Server socket closed~n")
        end.
    

      客户端:

    nano_client_eval(Str) ->
        %% 链接指定主机的2345端口, 发送数据时包头设置为4字节长
        {ok, Socket} = gen_tcp:connect("localhost", 2345, [binary, {packet, 4}]),
        %% 调用term_to_binary进行数据转换后向服务端发送数据
        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.
    

      运行结果:

    # 首先启动服务端
    1> socket_examples:start_nano_server().
    
    # 然后打开另一个erl窗口启动客户端
    # 随后客户端将服务端的计算结果接收后打印输出
    1> socket_examples:nano_client_eval("list_to_tuple([2+3*4, 10+20])").
    Client received binary = <<131,104,2,97,14,97,30>>
    Client result = {14,30}
    ok
    
    # 切换到服务端的erl窗口可以看到如下输出
    Server received binary = <<131,107,0,29,108,105,115,116,95,116,111,95,116,117,
                               112,108,101,40,91,50,43,51,42,52,44,32,49,48,43,50,
                               48,93,41>>
    Server (unpacked) "list_to_tuple([2+3*4, 10+20])"
    Server replying = {14,30}
    Server socket closed
    ok
    

      而这里服务端对客户端提交的字符串表达式进行计算的实现在string2value函数中

    string2value(Str) ->
        %% 按字符分解字符串
        {ok, Tokens, _} = erl_scan:string(Str ++ "."),
        %% 生成解析表达式
        {ok, Exprs} = erl_parse:parse_exprs(Tokens),
        Bindings = erl_eval:new_bindings(),
        %% 运行表达式
        {value, Value, _} = erl_eval:exprs(Exprs, Bindings),
        Value.
    

    14.1.3 改进服务器

    • 顺序型服务器
      一次只接收一个连接
      %% 接收连接后处理请求然后再次调用seq_loop等待下一个连接 
      start_seq_server() ->
          {ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4}, {reuseaddr, true}, {active, true}]),
          seq_loop(Listen).
      seq_loop(Listen) ->
          {ok, Socket} = gen_tcp:accept(Listen),
          loop(Socket),
          seq_loop(Listen).
      
    • 并行服务器
      一次可以接收多个并行连接
      %% 接收连接后启动新的进程来处理套接字 
      start_parallel_server() ->
          {ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 4}, {reuseaddr, true}, {active, true}]),
          spawn(fun() ->par_connect(Listen) end).
      par_connect(Listen) ->
          {ok, Socket} = gen_tcp:accept(Listen),
          spawn(fun() ->par_connect(Listen) end),
          loop(Socket).
      

    14.2 控制逻辑

    14.2.1 主动型消息接收(非阻塞)

    建立主动套接字后, 一个独立的客户机可能向服务端无限制的发送成千上万条消息, 如果超过了服务器的处理速度, 则可能导致系统崩溃。因为其不会阻塞客户端, 因此被称为异步服务器, 实现形式如下:

    %% 设置active为true即为异步方式 
    {ok, Listen} = gen_tcp:listen(Port, [..., {active, true}, ...]),
    {ok, Socket} = gen_tcp:accept(Listen),
    loop(Socket).
    
    loop(Socket) ->
        receive
            {tcp, Socket, Data}  ->
                %% 数据处理
            {tcp_closed, Socket} ->
                ...
        end.
    

    14.2.2 被动型消息接收(阻塞)

    建立被动套接字后, 只有服务端调用gen_tcp:recv(Socket, N)时才会接收来自套接字的数据, 且只接收N字节的数据, 因此不会因为客户端的大量请求而导致崩溃, 实现形式如下:

    %% 设置active为false即为阻塞方式 
    {ok, Listen} = gen_tcp:listen(Port, [..., {active, false}, ...]),
    {ok, Socket} = gen_tcp:accept(Listen),
    loop(Socket).
    
    loop(Socket) ->
        case gen_tcp:recv(Socket, N) of
            {ok, B}  ->
                %% 数据处理
                loop(Socket);
            {error, closed} ->
                ...
        end.
    

    14.2.3 混合型模式(半阻塞)

    半阻塞模式的套接字是主动的但仅针对一个消息, 需要显式的调用inet:setopts重新激活以便接收下一个消息, 在此之前系统将处于阻塞状态, 实现形式如下:

    %% 设置active为once即为异步方式 
    {ok, Listen} = gen_tcp:listen(Port, [..., {active, once}, ...]),
    {ok, Socket} = gen_tcp:accept(Listen),
    loop(Socket).
    
    loop(Socket) ->
        receive
            {tcp, Socket, Data}  ->
                %% 数据处理
                inet:setopts(Socket, [{active, once}]),
                loop(Socket);
            {tcp_closed, Socket} ->
                ...
        end.
    

    14.3 连接从何而来

      使用函数inet:peername(Socket)可以获取客户端信息。

    inet:peername(Socket) -> {ok, {IP_Address, Port} | {error, Why}}
    

    14.4 套接字的出错处理

      测试代码

    %% 服务端接收数据后调用atom_to_list处理数据
    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.
    
    %% 客户端连接后发生二进制数据使atom_to_list发生异常
    error_test() ->
        spawn(fun() ->error_test_server() end),
        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.
    

      运行结果:

    # 服务端异常结果
    1> socket_examples:error_test_server().
    received:<<"123">>
       exception error: bad argument
         in function  atom_to_list/1
            called as atom_to_list(<<"123">>)
         in call from socket_examples:error_test_server_loop/1 (socket_examples.erl, line 120) 
    
    # 客户端异常结果
    1> socket_examples:error_test().
    
    =ERROR REPORT==== 5-Nov-2013::10:19:27 ===
    Error in process <0.50.0> with exit value: {{badmatch,{error,eaddrinuse}},[{socket_examples,error_test_server,0,[{file,"socket_examples.erl"},{line,113}]}]}
    
    connected to:#Port<0.2291>
    Any={tcp_closed,#Port<0.2291>}
    ok
    

    14.5 UDP

    14.5.1 最简单的UDP服务器和客户机

      UDP服务器的形式

    server(Port) ->
        {ok, Socket} = gen_udp:open(Port, [binary]),
        loop(Socket).
    
    loop(Socket) ->
        receive
            {udp, Socket, Host, Port, Bin} ->
                BinReply = ... ,
                gen_udp:send(Socket, Host, Port, BinReply),
                loop(Socket)
        end.
    

      UDP客户机的形式

    client(Request) ->
        {ok, Socket} = gen_udp:open(0, [binary]),
        ok = gen_udp:send(Socket, "localhost", 4000, Request),
        Value = receive
                    {udp, Socket, _, _, Bin} ->{ok, Bin}
                %% 因为UDP协议传输的不可靠性, 有可能没有得到服务端的回应, 因此这里要设置超时时间 
                after 2000 ->error
                end,
        gen_udp:close(Socket),
        Value.
    

    14.5.2 一个计算阶乘的UDP服务器

      服务端实现:

    start_server() ->
        spawn(fun() ->server(40000) end).
    
    server(Port) ->
        {ok, Socket} = gen_udp:open(Port, [binary]),
        io:format("server opened socket:~p~n", [Socket]),
        loop(Socket).
    
    loop(Socket) ->
        receive
            {udp, Socket, Host, Port, Bin} = Msg ->
                io:format("server received:~p~n", [Msg]),
                N = binary_to_term(Bin),
                Fac = fac(N),
                gen_udp:send(Socket, Host, Port, term_to_binary(Fac)),
                loop(Socket)
        end.
    
    fac(0) ->1;
    fac(N) ->N * fac(N-1).
    

      客户端实现:

    client(N) ->
        {ok, Socket} = gen_udp:open(0, [binary]),
        io:format("client opened socket=~p~n", [Socket]),
        ok = gen_udp:send(Socket, "localhost", 40000, term_to_binary(N)),
        Value = receive
                    {udp, Socket, _, _, Bin} = Msg ->
                        io:format("client received:~p~n", [Msg]),
                        binary_to_term(Bin)
                after 2000 ->0
                end,
        gen_udp:close(Socket),
        Value.
    

      运行结果:

    1> udp_test:start_server().
    server opened socket:#Port<0.2308>
    <0.68.0>
    2> udp_test:client(40).
    client opened socket=#Port<0.2309>
    server received:{udp,#Port<0.2308>,{127,0,0,1},54449,<<131,97,40>>}
    client received:{udp,#Port<0.2309>,
                         {127,0,0,1},
                         40000,
                         <<131,110,20,0,0,0,0,0,64,37,5,255,100,222,15,8,126,242,
                           199,132,27,232,234,142>>}
    815915283247897734345611269596115894272000000000
    

    14.5.3 关于UDP协议的其他注意事项

    因为UDP数据报可能被传输两次, 因此为了避免这个问题, 可以使用make_ref函数为请求创建唯一标示。
    客户端实现:

    client(Request) ->
        {ok, Socket} = gen_udp:open(0, [binary]),
        Ref  = make_ref(),
        B1 = term_to_binary(Ref, Request),
        ok = gen_udp:send(Socket, "localhost", 40000, B1),
        wait_for_ref(Socket, Ref).
    
    wait_for_ref(Socket, Ref) ->
        receive
            {udp, Socket, _, _, Bin} ->
                case binary_to_term(Bin) of
                    %% 在client(Request)函数中已经为请求添加了唯一标示, 因此这里要从 {Ref, Val} 这种格式的数据中提取出真正的请求 
                    {Ref, Val} ->Val;
                    {_SomeOtherRef, _} ->
                        %% 对于其他数据则不用处理
                        wait_for_ref(Socket, Ref)
                end;
        after 1000 ->
            ...
        end. 
    

    14.6 向多台机器广播消息

    -module(broadcast).
    -compile(export_all).
    
    send(IoList) ->
        %% 获取网卡en0的IP信息
        case inet:ifget("en0", [broadaddr]) of
            {ok, [{broadaddr, Ip}]} ->
                %% 打开5010端口
                {ok, S} = gen_udp:open(5010, [{broadcast, true}]),
                %% 向本地网络的6000端口广播数据
                gen_udp:send(S, Ip, 6000, IoList),
                gen_udp:close(S);
            _ ->
                io:format("Bad interface name, or broadcastng not supported
    ")
        end.
    
    listen() ->
        %% 监听6000端口的广播
        {ok, _} = gen_udp:open(6000),
        loop().
    
    loop() ->
        receive
            Any ->
                %% 打印任何收到的信息
                io:format("received:~p~n", [Any]),
                loop()
        end.
    

      在单台机器上测试:

    # 一个shell打开监听 
    1> broadcast:listen().
    
    # 一个shell发送广播
    1> broadcast:send(["test"]). 
    
    # 可以看到监听端的输出
    1> broadcast:listen().      
    received:{udp,#Port<0.2337>,{10,0,1,224},5010,"test"} 
    

    Date: 2014-01-06 14:17:22 CST

    Author: matrix

    Org version 7.8.11 with Emacs version 24

    Validate XHTML 1.0
     
  • 相关阅读:
    CMake 手册详解(五)
    linux 学习资料、Linux学习书籍(入门书籍、shell编程)推荐
    linux shell 管道命令(pipe)使用及与shell重定向区别
    linux shell “(())” 双括号运算符使用
    web签名验证程序【跨服务器、中文字符签名方法】php为例
    linux shell 脚本实现tcp/upd协议通讯(重定向应用)
    web程序乱码深入分析【基础原理篇】php为例
    php empty,isset,is_null比较(差异与异同)
    php 实现进制转换(二进制、八进制、十六进制)互相转换
    php通过文件头检测文件类型通用类(zip,rar…)
  • 原文地址:https://www.cnblogs.com/scheme/p/3507122.html
Copyright © 2011-2022 走看看