为了熟悉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 messages
handle_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 client
getPid()->
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 table
bindPid(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 ets
ets: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 clientSessions
broadCastMsg(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 dwmsg
io: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 sender
getMembers(From)->
ok.
%%set clientinfo return ok or false
%% when ok broadcast change
%% user can changge name later
setInfo(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 type
handle_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),
1
end
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 room
handle_info({dwmsg,Message},State)->
io:format("client_session dwmsg recived ~p~n",[Message]),
case gen_tcp:send(State#clientinfo.socket, Message#message.content)of
ok->
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 server
start(Port)->
case (do_init(Port))of
{ok,ListenSocket}->
accept_loop(ListenSocket);
_Els ->
error
end.
%%listen port
do_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 connection
accept_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_generator
process_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(){
@Override
public void actionPerformed(ActionEvent e) {
// TODO Auto-generated method stub
if("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(){
@Override
public void windowActivated(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowClosed(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowClosing(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowDeactivated(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowDeiconified(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public void windowIconified(WindowEvent e) {
// TODO Auto-generated method stub
}
@Override
public 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 block
e.printStackTrace();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void sendMsg(String msg){
try {
oust.write(msg.getBytes());
oust.flush();
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
@Override
public void run() {
// TODO Auto-generated method stub
InputStreamReader 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 block
System.out.println("recMsg error"+e.getMessage());
}
try {
Thread.sleep(500);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}
测试方法:
启动服务器端:
>echatServer:start().
启动两个客户端:
run as java Application
效果图:
发送:
发送:
服务器端打印出的日志:
Startedid generated 1
binding socket...
chat_room:insert record {clientinfo,1,<0.102.0>,#Port<0.1778>,undefined,
undefined,undefined,undefined,undefined,
undefined,undefined}
clientBinded
client_session bind socket
NewState {clientinfo,[1],
undefined,#Port<0.1778>,undefined,undefined,undefined,
undefined,undefined,undefined,undefined}
id generated 2
binding socket...
chat_room:insert record {clientinfo,2,<0.109.0>,#Port<0.1797>,undefined,
undefined,undefined,undefined,undefined,
undefined,undefined}
clientBinded
client_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 1
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}
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 found
client_session dwmsg recived {message,msg,
[2],
undefined,<<"hello a">>,undefined}
client_session dwmsg sended
feching talbe key is 1
client_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 found
client_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连接断开进行处理,下一步对这个进行修改。