Erlang 聊天室程序( 一)

来源:互联网 发布:人工智能在线 编辑:程序博客网 时间:2024/06/02 22:54

              为了熟悉Erlang的套接字编程开始编写一个“聊天室”程序。基本流程如下:

             1.服务器启动监听指定端口

             2.启动一个gen_server 作为聊天室进程,里面使用ets 保持当前所有客户端连接信息,并负责将某个客户端的消息广播到所有在线客户端

             3.服务器接受客户端连接,并绑定到一个gen_server进程

             4.客户端维护进程接受客户端发送的消息,调用聊天室进程函数进行广播

             5.客户端维护进程接受聊天室发送的消息,转发给客户端


             客户端信息包括id、pid、socket、nickname、sex、age、province 等。

             目前包括以下几个模块:

           echatServer.erl  :服务器端程序启动模块。

           chat_room.erl :聊天室模块,一个gen_server负责处理客户端请求,保存了所有客户端的连接信息。

           id_generator.erl:负责为每一个连接的客户端生成唯一ID

           client_session.erl:与客户端socket绑定的gen_server回调模块,接收和发送消息

           chat_acceptor.erl:负责监听端口和处理连接的客户端socket


           代码如下:

           echatServer.erl:

          

-module(echatServer).%%%% Include files%%%%%% Exported Functions%%-export([start/0]).%%%% API Functions%%start()->chat_room:start_link(),chat_acceptor:start(3377),ok.

           chat_room.erl:

%% Author: Administrator%% Created: 2012-2-18%% Description: TODO: Add description to chat_room%% 1.genPid for every client connection%% 2.broadcast message to all clientSessions-module(chat_room).-behaviour(gen_server).%%%% Include files%%-include("clientinfo.hrl").-include("message.hrl").-record(state,{}).%%%% Exported Functions%%-export([start_link/0,init/1,getPid/0,bindPid/2,broadCastMsg/1,logout/1]).-export([handle_call/3,handle_info/2,handle_cast/2,code_change/3,terminate/2]).%%%% API Functions%%start_link()->gen_server:start_link({local,?MODULE}, ?MODULE, [],[]).%%to init all%%1.start id_generator%%2.create session table to store clientinfo%%init([])->id_generator:start_link(),ets:new(clientinfo,[public, ordered_set, named_table, {keypos,#clientinfo.id} ]),{ok,#state{}}.handle_call({getpid,Id},From,State)->{ok,Pid}=client_session:start_link(Id),{reply,Pid,State};handle_call({remove_clientinfo,Ref},From,State)->Key=Ref#clientinfo.id,ets:delete(clientinfo, Key);handle_call({sendmsg,Msg},From,State)->Key=ets:first(clientinfo),io:format("feching talbe key is ~p~n",[Key]),sendMsg(Key,Msg),{reply,ok,State}.%%process messageshandle_info(Request,State)->{noreply,State}.handle_cast(_From,State)->{noreply,State}.terminate(_Reason,_State)->ok.code_change(_OldVersion,State,Ext)->{ok,State}.%%%% Local Functions%%%% generate new Pid for eache conecting clientgetPid()->Id=id_generator:getnewid(client),Pid=gen_server:call(?MODULE,{getpid,Id}),io:format("id generated ~w~n",[Id]),#clientinfo{id=Id,pid=Pid}.%%bind Pid to Socket%%create new record and store into tablebindPid(Record,Socket)->io:format("binding socket...~n"),case gen_tcp:controlling_process(Socket, Record#clientinfo.pid) of{error,Reason}->io:format("binding socket...error~n");ok ->NewRec =#clientinfo{id=Record#clientinfo.id,socket=Socket,pid=Record#clientinfo.pid},io:format("chat_room:insert record ~p~n",[NewRec]),%store clientinfo to etsets:insert(clientinfo, NewRec),%then we send info to clientSession to update it's State (Socket info)Pid=Record#clientinfo.pid,Pid!{bind,Socket},io:format("clientBinded~n")%start client reciving%Pid!{start,Pid}end.%%generate random name%%and call setInfo(name)generatename()->ok.%%broad CastMsg to all connected clientSessionsbroadCastMsg(Msg)->gen_server:call(?MODULE, {sendmsg,Msg}).sendMsg(Key,Msg)->case ets:lookup(clientinfo, Key)of[Record]->io:format("Record found ~p~n",[Record]),Pid=Record#clientinfo.pid,%while send down we change msg type to dwmsgio:format("send smg to client_session ~p~n",[Pid]),Pid!{dwmsg,Msg},Next=ets:next(clientinfo, Key),sendMsg(Next,Msg);[]->io:format("no clientinfo found~n")end,ok;sendMsg([],Msg)->ok.%%return all connected clientinfo to sendergetMembers(From)->ok.%%set clientinfo return ok or false%% when ok broadcast change%% user can changge name latersetInfo(ClientInfo,From)->ok.logout(Ref)->gen_server:call(?MODULE, {remove_clientinfo,Ref}),ok.
        id_generator.erl:

%% Author: Administrator%% Created: 2012-2-16%% Description: TODO: Add description to id_generator-module(id_generator).-behavior(gen_server).%%%% Include files%%%%%% Exported Functions%%-export([start_link/0,getnewid/1]).-export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2,code_change/3]).-record(ids,{idtype,ids}).-record(state,{}).%%%% API Functions%%start_link()->gen_server:start_link({local,?MODULE}, ?MODULE, [],[]).init([])->mnesia:start(),io:format("Started"),mnesia:create_schema([node()]),case mnesia:create_table(ids,[{type,ordered_set},  {attributes,record_info(fields,ids)},  {disc_copies,[]} ]) of{atomic,ok}->{atomic,ok};{error,Reason}->io:format("create table error")end,{ok,#state{}}.getnewid(IdType)->%case mnesia:wait_for_tables([tbl_clientid], 5000) of%ok->%gen_server:call(?MODULE, {getid,IdType});%{timeout,_BadList}->%{timeout,_BadList};%{error,Reason}->%{error,Reason}%end    mnesia:force_load_table(ids),gen_server:call(?MODULE, {getid,IdType}).%%generate new Id with given typehandle_call({getid,IdType},From,State)->F=fun()->Result=mnesia:read(ids,IdType,write),case Result of[S]->Id=S#ids.ids,NewClumn=S#ids{ids=Id+1},mnesia:write(ids,NewClumn,write),Id;[]->NewClumn=#ids{idtype=IdType,ids=2},mnesia:write(ids,NewClumn,write),1end end, case mnesia:transaction(F)of  {atomic,Id}->  {atomic,Id};  {aborted,Reason}->  io:format("run transaction error ~1000.p ~n",[Reason]),  Id=0;   _Els->          Id=1000 end, {reply,Id,State}.handle_cast(_From,State)->{noreply,ok}.handle_info(Request,State)->{noreply,ok}.terminate(_From,State)->ok.code_change(_OldVer,State,Ext)->{ok,State}.%%%% Local Functions%%

       client_session.erl:

%% Author: Administrator%% Created: 2012-2-16%% Description: TODO: Add description to client_session-module(client_session).-behavior(gen_server).%%%% Include files%%-include("clientinfo.hrl").-include("message.hrl").%%%% Exported Functions%%-export([init/1,start_link/1,handle_info/2,handle_call/3,terminate/2]).-export([process_msg/1]).%%%% API Functions%%start_link(Id)->gen_server:start_link(?MODULE, [Id], [])%gen_server:start_link({local,?MODULE}, ?MODULE, [Id],[]).init(Id)->State=#clientinfo{id=Id},{ok,State}.%%handle message send from roomhandle_info({dwmsg,Message},State)->io:format("client_session dwmsg recived ~p~n",[Message]),case gen_tcp:send(State#clientinfo.socket, Message#message.content)ofok->io:format("client_session dwmsg sended ~n");{error,Reason}->io:format("client_session dwmsg sended error ~p ~n",Reason)end,{noreply,State};%%handle message recived from client%handle_info(Message,State) when is_binary(Message)->handle_info({bind,Socket},State)->io:format("client_session bind socket ~n"),NewState=State#clientinfo{socket=Socket},io:format("NewState ~p~n",[NewState]),{noreply,NewState};%to start reciving%handle_info({start,Pid},State)->%io:format("client_session:reciving start...~p~n",[State#clientinfo.socket]),%NewState=State#clientinfo{pid=Pid},%        process_msg(NewState),%{noreply,State};handle_info({tcp,Socket,Data},State)->io:format("client_session tcp data recived ~p~n",[Data]),%io:format("msg recived ~p~n",[Message]),NewMsg=#message{type=msg,from=State#clientinfo.id,content=Data},chat_room:broadCastMsg(NewMsg),{noreply,State};handle_info({tcp_closed,Socket},State)->chat_room:logout(State);handle_info(stop,State)->io:format("client stop"),{stop,normal,stopped,State};handle_info(Request,State)->io:format("client_session handle else ~p~n",[Request]),{noreply,State}.handle_call(Request,From,State)->{reply,ok,State}.handle_cast(Request,State)->{noreply,State}.terminate(_Reason,State)->ok.%%%% Local Functions%%process_msg(State)->io:format("client_session:process_msg SOCKET:~p ~n",[State#clientinfo.socket]),case gen_tcp:recv(State#clientinfo.socket, 0) of{ok,Message}->    io:format("recived ~p ~n",[Message]),%io:format("msg recived ~p~n",[Message]),NewMsg=#message{type=msg,from=State#clientinfo.id,content=Message},chat_room:broadCastMsg(NewMsg),process_msg(State);{error,closed}->io:format("client_session:recive error ~n"),process_msg(State);Any->io:format("client_session:recive any ~n"),process_msg(State)end.

         chat_acceptor.erl:

%% Author: Administrator%% Created: 2012-2-18%% Description: TODO: Add description to chat_acceptor-module(chat_acceptor).%%%% Include files%%%%%% Exported Functions%%-export([start/1,accept_loop/1]).%%%% API Functions%%%%start listen serverstart(Port)->case (do_init(Port))of{ok,ListenSocket}->accept_loop(ListenSocket);_Els ->error    end.%%listen portdo_init(Port) when is_list(Port)->start(list_to_integer(Port));do_init([Port]) when is_atom(Port)->start(list_to_integer(atom_to_list(Port)));do_init(Port) when is_integer(Port)->Options=[binary,             {packet, 0},             {reuseaddr, true},            {backlog, 1024},            {active, true}],case gen_tcp:listen(Port, Options) of{ok,ListenSocket}->{ok,ListenSocket};{error,Reason} ->  {error,Reason}end.%%accept client connectionaccept_loop(ListenSocket)->case (gen_tcp:accept(ListenSocket, 3000))of{ok,Socket} ->process_clientSocket(Socket),?MODULE:accept_loop(ListenSocket);{error,Reason} ->     ?MODULE:accept_loop(ListenSocket);{exit,Reason}->?MODULE:accept_loop(ListenSocket)end.%%process client socket%%we should start new thread to handle client%%generate new id using id_generatorprocess_clientSocket(Socket)->Record=chat_room:getPid(),chat_room:bindPid(Record, Socket),ok.%%%% Local Functions%%

                  为了测试这个服务器程序,我用JAVA写了个简单的client端程序,如下:

    MainForm.java

package com.kinglong.socket;import java.awt.BorderLayout;import java.awt.FlowLayout;import java.awt.GridBagConstraints;import java.awt.GridBagLayout;import java.awt.Insets;import java.awt.event.ActionEvent;import java.awt.event.ActionListener;import java.awt.event.WindowEvent;import java.awt.event.WindowListener;import javax.swing.JButton;import javax.swing.JFrame;import javax.swing.JPanel;import javax.swing.JTextArea;import javax.swing.JTextField;public class MainForm {private JFrame frame;private JPanel toolbar;private JTextArea outfile;private JTextField inpfile;SocketClient clientThread;public MainForm(){final JButton send = new JButton("send");outfile = new JTextArea();inpfile = new JTextField();clientThread = new SocketClient("***.***.***",3377,this);clientThread.start();send.setActionCommand("send");toolbar=new JPanel();toolbar.setLayout(new BorderLayout());toolbar.add(outfile,BorderLayout.CENTER);JPanel bottom =new JPanel();bottom.setLayout(new GridBagLayout());bottom.add(send,new GridBagConstraints(0,0,1,1,                  0.0,0.0,                  GridBagConstraints.CENTER,                  GridBagConstraints.NONE,                  new Insets(2,2,2,2),                  0,0));bottom.add(inpfile,new GridBagConstraints(1,0,1,1,                  0.0,0.0,                  GridBagConstraints.CENTER,                  GridBagConstraints.NONE,                  new Insets(2,2,2,2),                  200,0));toolbar.add(bottom,BorderLayout.SOUTH);ActionListener act = new ActionListener(){@Overridepublic void actionPerformed(ActionEvent e) {// TODO Auto-generated method stubif("send".equals(e.getActionCommand())){clientThread.sendMsg(inpfile.getText());inpfile.setText("");}}};send.addActionListener(act);frame = new JFrame();frame.getContentPane().add(toolbar);frame.setSize(500,300);frame.setResizable(false);frame.setVisible(true);frame.addWindowListener(new WindowListener(){@Overridepublic void windowActivated(WindowEvent e) {// TODO Auto-generated method stub}@Overridepublic void windowClosed(WindowEvent e) {// TODO Auto-generated method stub}@Overridepublic void windowClosing(WindowEvent e) {// TODO Auto-generated method stub}@Overridepublic void windowDeactivated(WindowEvent e) {// TODO Auto-generated method stub}@Overridepublic void windowDeiconified(WindowEvent e) {// TODO Auto-generated method stub}@Overridepublic void windowIconified(WindowEvent e) {// TODO Auto-generated method stub}@Overridepublic void windowOpened(WindowEvent e) {// TODO Auto-generated method stub}});}public void connect(){}public void recMsg(String msgs){String data =outfile.getText();outfile.setText((data==null?"":data)+msgs+"\n");}public static void main(String args[]){MainForm form = new MainForm();form.connect();}}

             SocketClient.java

package com.kinglong.socket;import java.io.BufferedReader;import java.io.IOException;import java.io.InputStream;import java.io.InputStreamReader;import java.io.OutputStream;import java.net.Socket;import java.net.UnknownHostException;public class SocketClient extends Thread{Socket clientSocket;InputStream inst;OutputStream oust;MainForm mf;boolean isrunning=true;public SocketClient(String ip,int port,MainForm mf){try {clientSocket= new Socket(ip,port);inst = clientSocket.getInputStream();oust = clientSocket.getOutputStream();this.mf=mf;} catch (UnknownHostException e) {// TODO Auto-generated catch blocke.printStackTrace();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}public void sendMsg(String msg){try {oust.write(msg.getBytes());oust.flush();} catch (IOException e) {// TODO Auto-generated catch blocke.printStackTrace();}}@Overridepublic void run() {// TODO Auto-generated method stubInputStreamReader reader = new InputStreamReader(inst);BufferedReader bfreader = new BufferedReader(reader);while(isrunning){String str=null;try {byte[] data =new byte[200];int len =0;while((len=inst.read(data))>0){str=new String(data).trim();System.out.println("msg recived:"+str);mf.recMsg(str);}} catch (IOException e) {// TODO Auto-generated catch blockSystem.out.println("recMsg error"+e.getMessage());}try {Thread.sleep(500);} catch (InterruptedException e) {// TODO Auto-generated catch blocke.printStackTrace();}}}}



                  测试方法:

                  启动服务器端:

                       >echatServer:start().

                  启动两个客户端:

                        run as java Application

                  效果图:

发送:

发送:


                 服务器端打印出的日志:

Startedid generated 1binding socket...chat_room:insert record {clientinfo,1,<0.102.0>,#Port<0.1778>,undefined,                                    undefined,undefined,undefined,undefined,                                    undefined,undefined}clientBindedclient_session bind socket NewState {clientinfo,[1],                     undefined,#Port<0.1778>,undefined,undefined,undefined,                     undefined,undefined,undefined,undefined}id generated 2binding socket...chat_room:insert record {clientinfo,2,<0.109.0>,#Port<0.1797>,undefined,                                    undefined,undefined,undefined,undefined,                                    undefined,undefined}clientBindedclient_session bind socket NewState {clientinfo,[2],                     undefined,#Port<0.1797>,undefined,undefined,undefined,                     undefined,undefined,undefined,undefined}client_session tcp data recived <<"hello a">>feching talbe key is 1Record found {clientinfo,1,<0.102.0>,#Port<0.1778>,undefined,undefined,                         undefined,undefined,undefined,undefined,undefined}send smg to client_session <0.102.0>Record found {clientinfo,2,<0.109.0>,#Port<0.1797>,undefined,undefined,                         undefined,undefined,undefined,undefined,undefined}client_session dwmsg recived {message,msg,                                      [2],                                      undefined,<<"hello a">>,undefined}send smg to client_session <0.109.0>client_session dwmsg sended no clientinfo foundclient_session dwmsg recived {message,msg,                                      [2],                                      undefined,<<"hello a">>,undefined}client_session dwmsg sended feching talbe key is 1client_session tcp data recived <<"hello b">>Record found {clientinfo,1,<0.102.0>,#Port<0.1778>,undefined,undefined,                         undefined,undefined,undefined,undefined,undefined}send smg to client_session <0.102.0>Record found {clientinfo,2,<0.109.0>,#Port<0.1797>,undefined,undefined,                         undefined,undefined,undefined,undefined,undefined}send smg to client_session <0.109.0>no clientinfo foundclient_session dwmsg recived {message,msg,                                      [1],                                      undefined,<<"hello b">>,undefined}client_session dwmsg sended client_session dwmsg recived {message,msg,                                      [1],                                      undefined,<<"hello b">>,undefined}client_session dwmsg sended 

至此基本的聊天功能实现了。顺便说下,以上的服务器端程序将监听到的socket连接交由单独的进程处理了。

还有不足就是客户端退出时会引发服务器端异常退出,这个需要对socket连接断开进行处理,下一步对这个进行修改。