第17章 套接字编程

1.使用TCP从服务器获取数据

%%socket_examples.erl
-module(socket_examples).
-export([nano_get_url/0,nano_get_url/1,receive_data/2]).

nano_get_url() ->
    nano_get_url("www.baidu.com").

nano_get_url(Host) ->
    {ok,Socket}=gen_tcp:connect(Host,80,[binary,{packet,0}]),
    ok=gen_tcp:send(Socket,"Get / HTTP/1.0\r\n\r\n"),
    receive_data(Socket,[]).

receive_data(Socket,SoFar) ->
    receive
        {tcp,Socket,Bin} ->
            receive_data(Socket,[Bin|SoFar]);
        {tcp_closed,Socket} ->
            list_to_binary(lists:reverse(SoFar))
    end.
  • 工作方式:
  • 1)调用gen_tcp:connect来打开一个到www.baidu.com80端口的TCP套接字。连接调用里的binary参数告诉系统要以“二进制”模式打开套接字,并把所有数据用二进制型传给应用程序。{packet,0}的意思是把未经修改的TCP数据直接传给应用程序。
  • 2) 调用gen_tcp:send,把消息GET / HTTP/1.0\r\n\r\n发送给套接字,然后等待回复。这个回复并不是放在一个数据包里,而是分成多个片段,一次发送一点。这些片段会被接收成为消息序列,发送给打开(或控制)套接字的进程。
  • 3)收到一个{tcp,Socket,Bin}消息。这个元组的第三个参数是一个二进制型,原因是打开套接字时使用了二进制模式。这个消息是Web服务器发送给我们的数据片段之一。把它添加到目前已收到的片段列表中,然后等待下一个片段。
  • 4)收到一个{tcp_closed, Socket}消息。这会在服务器完成数据发送时发生。
  • 5)当所有片段都到达后,因为它们的保存顺序是错误的,所以反转它们并连接所有片段。
  • 调用示例:B=socket_examples:nano_get_url().
  • 转换输出格式: io:format("~p~n",[B]). 或是 string:tokens(binary_to_list(B),"\r\n").

2.一个简单的TCP服务器

%%socket_server.erl:
-module(socket_server).
-export([start_nano_server/0,loop/1,nano_client_eval/1]).

start_nano_server() ->
    {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=lib_misc: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) ->
    {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.
  • 工作方式:
  • 1)首先,调用gen_tcp:listen来监听2345端口的连接,并设置消息的打包约定。{packet,4}的意思是每个应用程序消息前部都有一个4字节的长度包头。然后gen_tcp:listen(..)会返回{ok, Listen}或{error, Why},但我们只关心能够打开套接字的返回值。因此,编写如下代码 {ok,Listen}=gen_tcp:listen(....).
  • 这会让程序在gen_tcp:listen返回{error, ...}时抛出一个模式匹配异常错误。在成功的情况下,这个语句会绑定Listen到刚监听的套接字上。我们只能对监听端口做一件事,那就是把它用作gen_tcp:accept的参数。
  • 2)现在调用gen_tcp:accept(Listen)。在这个阶段,程序会挂起并等待一个连接。当我们收到连接时,这个函数就会返回变量Socket,它绑定了可以与连接客户端通信的套接字。
  • 3)在accept返回后立即调用gen_tcp:close(Listen)。这样就关闭了监听套接字,使服务器不再接收任何新连接。这么做不会影响现有连接,只会阻止新连接。
  • 4)解码输入数据
  • 5)然后执行字符串
  • 6)然后编码回复数据并把它发回套接字
  • 7)同时定义一个客户端.
  • 应用示例:(注意要开启两个shell窗口)
  • socket_server:start_nano_server().
  • socket_server:nano_client_eval("lsit_to_tuple([2+3*4,10+20])").

3.顺序和并行服务器

  • 1)顺序服务器
    将源代码: 
    start_nano_server() -> 
        {ok,Listen}=gen_tcp:listen(....), 
        {ok,Socket}=gen_tcp:accept(Listen), 
        loop(Socket)....
    修改为:
    start_seq_server() -> 
        {ok,Listen} =gen_tcp:listen(....), 
        seq_loop(Listen).
        seq_loop(Listen) -> 
            {ok,Socket}=gen_tcp:accept(Listen), 
            loop(Socket), 
            seq_loop(Listen). 
    loop(....) .... %和以前一样
  • 2)并行服务器:每当gen_tcp:accept收到一个新连接时就立即分裂一个新进程
    将源码:
    start_parallel_server() -> 
        {ok,Listen}=gen_tcp:listen(...), 
        spawn(fun() -> 
            par_connect(Listen) end).
    改为:
    par_connect(Listen) -> 
        {ok,Socket}=gen_tcp:accept(Listen), 
        spawn(fun() -> par_connect(Listen )end), 
        loop(Socket). 
    loop(...).....%和之前一样

4.注意点:

  • 1)创建某个套接字(通过调用gen_tcp:accept或gen_tcp:connect)的进程被称为该套接字的控制进程。所有来自套接字的消息都会被发送到控制进程。如果控制进程挂了,套接字就会被关闭。某个套接字的控制进程可以通过调用gen_tcp:controlling_process(Socket, NewPid)修改成NewPid
  • 2)我们的并行服务器可能会创建出几千个连接,所以可以限制最大同时连接数。实现的方法可以是维护一个计数器来统计任一时刻有多少活动连接。每当收到一个新连接时就让计数器加1,每当一个连接结束时就让它减1。可以用它来限制系统里的同时连接总数。
  • 3)接受一个连接后,显式设置必要的套接字选项是一种很好的做法,就像这样:
    {ok,Socket}=gen_tcp:accept(Listen), 
    inet:setopts(Socket,[{packet,4},binary,{nodelay,true},{active,true}]), 
    loop(Socket)
  • 4)Erlang 的 R11B-3 版开始允许多个 Erlang 进程对同一个监听套接字调用 gen_tcp:accept/1。这让编写并行服务器变得简单了,因为你可以生成一个预先分裂好的进程池,让它们都处在gen_tcp:accept/1的等待状态。

5.主动和被动套接字

  • 1)三种打开模式:主动(active),单次主动(active once) ,被动(passive) . 通过在gen_tcp:connect(Address, Port, Options)或gen_tcp:listen(Port, Options)的Options参数里加入{active, true | false | once}选项实现的。
  • 2)主动信息接收(非阻塞式)
    {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. %当客户端生成数据的速度快于服务器处理数据的速度,系统就会遭受数据洪流的冲击
  • 3)被动信息接收(阻塞式)
    {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. %每次想要接收数据就调用gen_tcp:recv,否则客户端会一直阻塞.
  • 4)混合信息接收(部分阻塞式)
    {ok,Listen}=gen_tcp:listen(Port,[....,{active,once}...]),
    {ok,Socket}=gen_tcp:accept(Listen),
    loop(Socket).
    loop(Socket) ->
        receive
            {tcp,Socket,Data} ->
                .......对数据进行操作....
                %当你准备好启动下一个信息的接收时
                inet:setopts(Sock,[{active,once}]),
                loop(Socket);
            {tcp_closed,Socket}->
                .....
        end. %当控制进程收到一个消息后,必须显式调用inet:setopts才能重启下一个消息的接收,否则系统处于阻塞状态.

6.套接字错误处理

  • 当服务器因为程序错误挂了,那么服务器支配的套接字就会被自动关闭,同时向客户端发送一个{tcp_closed,Socket}消息

7.UDP

%应用示例:一个UDP阶乘服务器
-module(udp_test).
-export([start_server/0,client/1]).
start_server() ->
    spawn(fun() -> server(4000) 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",4000,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.

8.UDP数据包可能会二次传输,所以可调用Erlang的内置函数make_ref,可以确保返回一个全局唯一的引用.

猜你喜欢

转载自blog.csdn.net/qq_34755443/article/details/84316635