2008年2月21日星期四

erlex : play erlang with flex

昨天完成了一个范例小程序,可以用来群聊。客户端是浏览器里的Flash,服务器端是erlang写的脚本。
基本框架是服务器监听两个端口,8765替代80用来让浏览器通过http协议装载html和swf文件,2345端口是原生Socket通信,负责聊天数据的推拉。
先来看看http的实现吧,在erlang脚本中只需要一句话:
inets:start(httpd, [{server_name,"erlex"},{bind_address, "59.66.121.94"},{port, 8765},{server_root,"www"}).
一个httpd就跑起来了(不用找Apache),然后把它放到后台去不用管了。这里server_root给的是相对路径,所有html,swf文件都在里面。
主要来看原生Socket的通讯。
1、客户端,熟悉Java的同志一定对ActionScript有一见如故的感觉。全部代码可以去
http://code.google.com/p/erlex/
看,我已经check in了,这里简单点一句
private function send(content:String):void{
    var buf:ByteArray = new ByteArray();
    buf.writeByte(0x02);
    buf.writeUTF(content);
    socket.writeBytes(buf,0,buf.length);
}
开了一段字节数组,先向里面丢了一个0x02,表示这个数据包是客户端说的话,后面是一个字符串,头两个字节表示跟着的字符数组的长度,跟着的字符数组是字符串的UTF8表示。socket在write之后应该flush一下,目前问题不大。
2、服务器端,大多数同志可能对erlang不是很熟悉,我多讲几句吧。
因为要支持多个客户端同时连接,因此用到了erlang的拿手好戏:并发编程。我们为每一个Socket连接准备一个erlang的process(可以理解为Java的线程,但是相当轻量级,官方测试数据证明了erlang这方面的性能),同时准备一个总线process,做传递消息枢纽。
par_connect(Listen)->
    case gen_tcp:accept(Listen) of
    {ok, Socket} ->
        spawn(fun() -> par_connect(Listen) end),
        loop(Socket);
    end.
上面的函数用来捕获一个TCP连接,在loop中处理,并启动一个新process,执行同样的par_connect函数,这是一种并行递归。
loop(Socket)->
    receive
        {tcp, Socket, Bin} ->
            [H1,_,_|T1] = binary_to_list(Bin),
            case H1 of
            2 ->
                bus ! {broadcast, self(), T1},
                loop(Socket);
    end.
这个函数所在的process可能接收到各种消息,其中{tcp, Socket, Bin}是收到一个TCP数据包(由原子tcp确定),Bin中存放的是数据包的TCP正文二进制流。我们先把它转换成字节数组binary_to_list,再模式匹配[H1,_,_|T1],得到H1是第一个字节,表示数据包的用途,2的意思就是上面0x02,然后用,_,_忽略后面两个字节(如果在ActionScript中用writeUTFBytes就没有这个麻烦),用T1装字节数组后面的内容。然后通过
bus ! {broadcast, self(), T1},
发给总线。其中bus在程序开始注册为总线process,self()表示当前process id,可以用来确定说话人的身份。
值得注意的是最后又调用了loop方法似乎是递归,很快要堆栈溢出的,不过erlang编译器把他当作迭代调用,这是erlang风格,erlang有很多很特别的风格。我认为最酷的还是process间通信的风格,发送消息用!加term,收到消息用recieve加模式匹配,和分布式算法的伪代码描述一模一样!

总线查出说话人的名字,并把他说的话发给所有process
[{_,SpeakerName}|_] = ets:lookup(TableID,SpeakerPid),
lists:foreach(fun({Pid,Name}) -> Pid ! {send, SpeakerName, Content} end , ets:tab2list(TableID))
这里用到了list的遍历、闭包以及erlang数据表ets的操作。
loop函数中再加一个recieve子句
{send, SpeakerName, Content}->
    ok = gen_tcp:send(Socket, list_to_binary(SpeakerName++" says: "++Content)),
这就发回给客户端了,ActionScript中这样接收:
socket.addEventListener( ProgressEvent.SOCKET_DATA, onSocketData );
其中
private function onSocketData( event:ProgressEvent ):void {
    var data:String = socket.readUTFBytes(socket.bytesAvailable);
    history.text=history.text+"\n"+data.toString();
}
history是多行文本框。

事实上,利用ets可以避免使用总线,只是考虑多个process间不共享存储的分布式场景,才用的。erlang为分布式计算而生。

没有评论: