Erlang套接字编程(下篇)

四:UDP

UDP是一种无连接协议,意思是客户端向服务器发送消息之前不必建立连接。这就意味着UDP非常适合那些大量客户端向服务器发送简短消息的应用程序。用Erlang编写UDP客户端和服务器要比编写TCP程序简单得多,因为我们无需担心服务器连接的维护工作。

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.

这比TCP程序要容易一些,因为无需担心如何让进程接收“套接字关闭”的消息。请注意,我们用binary模式打开了套接字,它告诉驱动要把所有消息以二进制数据的形式发送给控制进程。接下来是客户端。这里有一个非常简单的客户端。它只是打开一个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}
            after 2000 ->
                error
            end,
gen_udp:close(Socket),
Value

必须设置一个超时,因为UDP是不可靠的,我们可能会得不到回复。

2.一个UDP阶乘服务器

可以很轻松地创建一个服务器,让它用发送给它的任意数字计算我们的老朋友——阶乘。这段代码建立在上面的基础之上:

%% udp_test.erl
-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.

%$ 客户端
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.

请注意,我添加了一些打印语句,这样就能看到程序运行时发生了什么。我在开发程序时总会添加一些打印语句,然后在程序能正常工作时编辑或注释掉它们。现在来运行这个示例。首先,启动服务器:

1> udp test:start_server().
server opened socket:#Port<0.106>
<0.34.0>

%% 它在后台运行,这样我们就可以生成一个请求40阶乘值的客户端请求。
2> udp test:client(40).
client opened socket=#Port<0.105>
server received:{udp,#Port<0.106>,{127,0,0,1},32785,<<131,97,40>>}
client received:{udp,#Port<0.105>,
                    {127,0,0,1},4000,
                    <<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

现在就得到了一个小小的UDP阶乘服务器。为了找点乐子,你可以试着编写这个程序的TCP版本,然后对它们进行基准测试来加以比较。

3.UDP数据包须知

UDP数据包可以传输两次(这出乎一些人的意料之外),所以在编写远程过程调用代码时一定要小心。第二次查询得到的回复可能只是第一次查询回复的复制。为防止这类问题,可以修改客户端代码来加入一个唯一的引用,然后检查服务器是否返回了这个引用。要生成一个唯一的引用,需要调用Erlang的内置函数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", 4000, B1),
    wait_for_ref(Socket, Ref).

wait_for_ref(Socket, Ref) ->
    receive
        {udp, Socket, _, _, Bin} ->
            case binary_to_term(Bin) of
                {Ref, Val} ->
                     %% 得到的是正确值
                     Val; 
                {_SomeotherRef, _} ->
                     %% 其他值则丢弃
                     wait_for_ref(Socket, Ref)
            end;
    after 1000 ->
        ...
    end.

以上就是UDP的相关介绍。UDP经常用于有低延迟要求的在线游戏,对它们来说是否偶尔丢包则无关紧要。

五:对多台机器广播

最后来看如何设立一个广播信道。它的代码很简单:

%% broadcast.erl
-module(broadcast).
-compile(export_all).

send(IoList) ->
    case inet:ifget("etho", [broadaddr]) of
        {ok, [{broadaddr, Ip}]} ->
            {ok, S} = gen_udp:open(5010, [{broadcast, true}]),
            gen_udp:send(S, Ip, 6000, IoList),
            gen_udp:closc(S);
        _ ->
            io:format("Bad interface name,or\n"
                      "broadcasting not supported\n")
    end.

listen() ->
    {ok, _} = gen_udp:open(6000),
    loop().

loop() ->
    receive
        Any ->
            io:format("received:~p~n", [Any]),
            loop()
    end.

在这里需要两个端口,一个发送广播,另一个监听回应。我们选择了5010端口来发送广播请求,6000端口用来监听广播(这两个数字没有特殊含义,我只是选择了系统里两个空闲的端口)。只有发送广播的进程才会打开5010端口,而网络上的所有机器都会调用broadcast:listen()来打开6000端口并监听广播消息。

六:一个 SHOUTcast 服务器

我们将运用新学到的套接字编程技术来编写一个SHOUTcast服务器。SHOUTcast是由Nullsoft公司开发的协议,它被用于传输音频数据流①。SHOUTcast使用HTTP作为传输协议来发送MP3或AAC编码的音频数据。

1.SHOUTcast协议

SHOUTcast协议很简单。

(1) 首先,客户端(例如XMMS、Winamp或iTunes)发送一个HTTP请求到SHOUTcast服务器。这是我在家里运行SHOUTcast服务器时XMMS生成的请求:

GET / HTTP/1.1
Host:localhost
User-Agent: xmms/1.2.10
Icy-MetaData:1

(2) SHOUTcast服务器的回复是:

ICY 200 OK
icy-noticel: <BR>This stream requires
    <a href=http://www.winamp.com/>;Winamp</a><BR>
icy-notice2: Erlang Shoutcast server<BR>
icy-name: Erlang mix
icy-genre: Pop Top 40 Dance Rock
icy-url: http://localhost:3000
content-type: audio/mpeg
icy-pub: 1
icy-metaint: 24576
icy-br: 96
...数据...

(3) 现在SHOUTcast服务器会发送连续的数据流。这个数据流具有如下结构:

FHFHFHF...

%% F是一个MP3音频数据块,它的长度必须刚好是24 576字节(icy-metaint参数的值)。
%% H是一个数据头,由单字节的K后接16*K字节的数据组成。
%% 因此,可以用二进制型表示的最小数据头是<<0>>。
%% 接下来的数据头可以这样表示:
                <<1,B1,B2,,B16>>
%% 它的数据部分是一个StreamTitle=' ... ';StreamUrl='http:// ...';形式的字符串,
%% 长度不足则在右边补零,直到填满整个数据头。

2.SHOUTcast服务器的伪代码

在展示最终程序之前,先来看看省略了细节部分的整体代码流:

start_parallel_server(Port) ->
    {ok,Listen} = gen_tcp:listen(Port, ...),
    %% 创建一个歌曲服务器,它了解我们的所有音乐
    PidSongServer = spawn(fun() -> songs() end),
    spawn(fun() -> par_connect(Listen, PidSongServer) end).

%% 为每个连接分裂一个这样的进程
par_connect(Listen, PidSongServer) ->
    {ok, Socket} = gen_tcp:accept(Listen),
    %% 在accept返回时分裂一个新进程来等待下一个连接
    spawn(fun() -> par_connect(Listen, PidSongServer) end),
    inet:setopts(Socket,[{packet,0}, binary, {nodelay, true},
                         {active,true}]),
    %% 处理请求
    get_request(Socket, PidsongServer, []).

%%等待TCP请求
get_request(Socket, PidSongServer, L) ->
    receive
        {tcp, Socket, Bin} ->
            ...Bin包含来自客户端的请求,
            ...如果请求是分段的就再次调用循环,
            ...否则调用got_request(Data,Socket,PidSongServer)
        {tcp_closed,Socket} ->
            ...这是针对客户端在发送请求之前就已中止的情况(非常罕见)
    end.

%% 我们接到了请求,发送一个回复
got_request(Data, Socket, PidSongServer) ->
    ...Data是来自客户端的请求...
    ...分析它...
    ...我们将始终满足请求...
    gen_tcp:send(Socket, [response()]),
    play_songs(Socket, PidSongServer).

%%持续播放歌曲直到客户端退出
play_songs(Socket, PidSongServer) ->
    ...PidSongServer持有一份MP3文件清单
    Song = rpc(PidsongServer, random_song),
    ...Song是一首随机歌曲...
    Header = make_header(Song),
    ...生成数据头...
    {ok, S} = file:open(File, [read,binary,raw]),
    send_file(1, S, Header, 1, Socket) ->,
    file:close(S),
    play_songs(Socket, PidSongServer).

send_file(K, S, Header, offSet, Socket) ->
    ...把文件分块发送给客户端...
    ...发送完整个文件就返回...
    ...但如采写入套接字出错则退出,
    ...这会发生在客户端退出时

如果你查看真实的代码,就会发现细节稍有不同,但它们的原理是相同的。完整的代码清单不会在这里展示,但可以在文件code/shout.erl里找到。

3.运行SHOUTcast服务器

要运行服务器并看看它能否工作,需要执行下列步骤。

(1) 制作一个播放列表。

(2) 启动服务器。

(3) 将一个客户端指向服务器。

制作列表需要以下三步。

(1) 移至代码目录。

(2) 编辑mp3_manager.erl文件里start1函数的路径,让它指向待输出音频文件所属目录的

根目录。

(3) 编译mp3_manager,然后输入命令mp3_manager:start1()。应该能看到如下输出:
1> c(mp3_manager).
{ok,mp3_manager}
2> mp3_manager:start1().
Dumping term to mp3data
ok
如果有兴趣,现在可以查看mp3data文件来了解分析结果。 现在可以启动SHOUTcast服务器了。
1> shout:start().
...

要测试这个服务器,请执行下列操作。

(1) 去另一个窗口打开某个音频播放器,将它指向http://localhost:3000上的流服务。

我的系统使用XMMS,命令如下:
xmms http://localhost:3000

如果用的是iMac上的iTunes,我就会在“高级” > “打开流”菜单里输入上面的URL来访问服务器。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

明明如皓

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值