四: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()。应该能看到如下输出:如果有兴趣,现在可以查看mp3data文件来了解分析结果。 现在可以启动SHOUTcast服务器了。1> c(mp3_manager). {ok,mp3_manager} 2> mp3_manager:start1(). Dumping term to mp3data ok
1> shout:start(). ...
要测试这个服务器,请执行下列操作。
(1) 去另一个窗口打开某个音频播放器,将它指向http://localhost:3000上的流服务。
我的系统使用XMMS,命令如下:xmms http://localhost:3000
如果用的是iMac上的iTunes,我就会在“高级” > “打开流”菜单里输入上面的URL来访问服务器。