作为新手的练习项目,使用erlang来实现一个聊天室是一个很好的练手形式,接下来讲解下我开发过程的思路和根据需求变化的版本的迭代升级.
初代版本1.0
对于聊天室的需求有以下几点:
1)用户登录
2)所有登陆的用户默认在大厅中,可以进行聊天
2)房间创建,创建者自动成为房主
3)进入房间,同一个房间里的人可以聊天
4)退出房间,当所有人退出房间时,10秒内若是没有人进入该房间,则房间销毁,若是期间有人进入,则自动升级为该房房主
5)房主具有禁言,解禁,踢人,过让房主权限
对于上述的最基本的需求,我使用了tcp并行连接,ets表存储信息,定义功能号等,最初始的开发模板是在另一个博客中找到的最为基础的版本,仅仅是实现了登录与消息发送功能.因为后面进行了数据存储方式和结构的改进,所以对于这一版本仅仅贴上一些代码
chat_client.erl
%%初始化
init([]) ->
get_socket(),
{ok, ets:new(mysocket, [public, named_table])}.
%获取socket
get_socket() ->
register(client, spawn(fun() -> {ok, Socket} = gen_tcp:connect("localhost", 2345, [binary, {packet, 0}]),
handle(Socket) end)).
%<<-------------------------回调函数----------------------------->>
%登录的时候添加socket
handle_cast({addSocket, UserName, Socket}, Tab) ->
case ets:lookup(Tab, UserName) of
[{UserName, Socket}] -> have_socket;
[] -> ets:insert(Tab, {UserName, Socket})
end,
{noreply, Tab}.
%<<-------------------------回调函数----------------------------->>
%登录接口
login(Name, Password) ->
% io:format("log1 ~p ~p ~n ",[Name,Password]),
client ! {self(), {login, Name, Password}},
receive
Response -> Response
end.
%聊天发送接口
send_message(Msg) ->
client ! {self(), {msg, Msg}},
receive
Response -> Response
end.
%------------------------------------------------
handle(Socket) ->
receive
%来自控制进程的请求
{From, Request} ->
case Request of
%登录的请求协议号0000
{login, Name, Password} ->
N = term_to_binary(Name),
P = term_to_binary(Password),
Packet = <<0000:16, (byte_size(N)):16, N/binary, (byte_size(P)):16, P/binary>>,
%定义前4个字节为协议号,名字字节长度占4字节,然后时名字字节,然后是密码字节长度占4字节,最后是密码字节
gen_tcp:send(Socket, Packet),
receive
{tcp, Socket, Bin} ->
<<State:16, Date/binary>> = Bin, %状态码
<<Size1:16, Date1/binary>> = Date, %登录成功信息长度
case binary_to_term(Date1) of
"success" ->
gen_server:cast(?SERVER, {addSocket, Name, Socket}),
From ! {"you have login successfully "};
"fail" ->
From ! {"you haved login failed,please try again "},
gen_tcp:close(Socket)
end
after 5000 ->
io:format("overTime1 ~n")
end,
handle(Socket);
%发送信息协议号0001
{msg, Msg} ->
case ets:match(mysocket, {'$1', Socket}) of %$1表示占位符,匹配所有符合条件的值,返回为[[..],[..],...]
[[Name]] -> N = term_to_binary(Name);
Name -> N = term_to_binary("noLogin")
end,
io:format("~ts : ~ts~n", [Name, Msg]),
M = term_to_binary(Msg),
Packet = <<0001:16, (byte_size(N)):16, N/binary, (byte_size(M)):16, M/binary>>,
gen_tcp:send(Socket, Packet),
receive
{tcp, Socket, Bin} ->
<<State:16, Date/binary>> = Bin, %状态码
<<Size1:16, Date1/binary>> = Date, %消息长度
ReceiveMsg = binary_to_term(Date1),
case ReceiveMsg of
{"ok", "received"} ->
From ! {"send success"};
{"failed", "noLogin"} ->
From ! {"you don't have logined "};
"silent" ->
From ! {"you are silented "};
Fail ->
io:format(" ~p~n", [ReceiveMsg]),
From ! {"failed "}
end
after 3000 ->
io:format("overTime2 ~n")
end,
handle(Socket)
end;
{tcp, Socket, Bin} ->
<<State:16, Date/binary>> = Bin, %状态码
<<Size1:16, Date1/binary>> = Date, %用户长度
<<User:Size1/binary, Date2/binary>> = Date1, %用户
<<Size2:16, Date3/binary>> = Date2, %消息的长度
<<Msg:Size2/binary, Date4/binary>> = Date3, %消息
io:format("~ts : ~ts~n", [binary_to_term(User), binary_to_term(Msg)]),
handle(Socket);
{tcp_closed, Socket} ->
io:format("receive server don't accept connection!~n")
end.
chat_server.erl
扫描二维码关注公众号,回复:
4531982 查看本文章
init([]) ->
initialize_ets(),
start_parallel_server(),
ets:new(myroom, [bag, public, named_table, {keypos, #room.username}]),
{ok, ets:new(mysocket, [public, named_table])}.%创建一个ets,名为mysocket,保存连接入的socket,注意该位置对应的是State
%开启服务器
start_parallel_server() ->
{ok, Listen} = gen_tcp:listen(2345, [binary, {packet, 0}, {reuseaddr, true}, {active, true}]),
spawn(fun() -> per_connect(Listen) end).
%每次绑定一个当前Socket后再分裂一个新的服务端进程,再接收新的请求
per_connect(Listen) ->
{ok, Socket} = gen_tcp:accept(Listen),
%io:format("Socket ~p",[Socket]), 输出结果: Socket #Port<0.2322>
spawn(fun() -> per_connect(Listen) end),
loop(Socket).
%初始化ets
initialize_ets() ->
ets:new(test, [set, public, named_table, {keypos, #user.name}]),
ets:insert(test, #user{id = 01, name = "carlos", passwd = "123", login_times = 0, chat_times = 0, last_login = {},
state = 0, roomnumber = 0}),
ets:insert(test, #user{id = 02, name = "qiqi", passwd = "123", login_times = 0, chat_times = 0, last_login = {},
state = 0, roomnumber = 0}),
ets:insert(test, #user{id = 03, name = "cym", passwd = "123", login_times = 0, chat_times = 0, last_login = {},
state = 0, roomnumber = 0}).
%-----------------初始函数----------------
%<<-------------------------回调函数----------------------------->>
handle_call({addSocket, UserName, Socket, RoomNumber}, _From, Tab) ->
Reply = case ets:lookup(Tab, UserName) of
[{UserName, Socket, RoomNumber, Owner, State, First}] -> have_socket;
[] -> Owner = "User", State = 1, First = 0,
ets:insert(Tab, {UserName, Socket, RoomNumber, Owner, State, First})
end,
{reply, Reply, Tab}.
%<<-------------------------回调函数----------------------------->>
%------------------3------------------
%接收信息并处理
loop(Socket) ->
io:format("<--------receiving the message-------->~n"),
receive
{tcp, Socket, Bin} ->
<<State:16, Date/binary>> = Bin, %将前4个字节作为状态码,其余部分作为二进制类型保存在Date中
<<Size1:16, Date1/binary>> = Date, %Size1第一个信息的长度的二进制格式
<<Str1:Size1/binary, Date2/binary>> = Date1,%Str1第一个信息的二进制格式
<<Size2:16, Date3/binary>> = Date2, %Size2第二个信息的长度的二进制格式
<<Str2:Size2/binary, Date4/binary>> = Date3,%Str2第二个信息的二进制格式
case State of
%登录
0000 ->
Name = binary_to_term(Str1),
case info_lookup(test, Name) of
[{user, Uid, Pname, Pwd, Logc, ChatC, Lastlog, LoginState, RoomNumber}] ->
addSocket(Pname, Socket, RoomNumber),
info_update(test, Pname, 5, Logc + 1),
%登录成功后,把logingState变为1.
info_update(test, Pname, 8, 1),
Message = "success",
sendback(Socket, State, Message),
io:format("~ts has logged~n", [Name]),
loop(Socket);
%为空表示该用户没有记录
[] -> io:format("you haved not registered yet"), %返回的是[] 而不是 [{}]
Message = "fail",
sendback(Socket, State, Message),
loop(Socket)
end;
%接收信息
0001 ->
Name = binary_to_term(Str1),
Msg = binary_to_term(Str2),
case info_lookup(mysocket, {Name, 5}) of
1 ->
[#user{chat_times = Ccount, state = LoginState, roomnumber = Number}] = info_lookup(test, Name),
io:format("Message -> ~nroom: ~p ~nUser ~ts~n", [Number, Name]),
%更新聊天次数
case LoginState of
1 -> info_update(test, Name, 6, Ccount + 1),
Message = {"ok", "received"},
sendback(Socket, State, Message),
io:format("message : ~ts ~n", [Msg]),
%广播信息
gen_server:call(?MODULE, {sendAllMessage, Name, Msg, Number}),
loop(Socket);
0 ->
Message = {"failed", "noLogin"},
sendback(Socket, State, Message),
io:format("user ~ts no login", [Name]),
loop(Socket)
end;
0 ->
Message = "silent",
sendback(Socket, State, Message),
loop(Socket)
end
end;
{tcp_closed, Socket} ->
io:format("Server socket closed~n")
end.
%-------------------3--------------------------
版本2.0
由于对性能方面有了相应的要求,所以需要考虑到ets在高并发访问时的锁表动作将会大大制约性能,所以考虑使用state(即实现函数传值)或是进程字典,由于开发的规范要求,所以首先推荐使用state进行数据的存储操作.
使用state进行数据存储,首先要明白我们需要存入多条数据,所以需要使用state+ record+list的方法,如以下示例:
-record(user,{username,socket,where}).
-record(state,datalist=[]).
init(ok,#state{}).
%往state中存储数据,注意当需要存储的数据只是在原有数据上的部分数据更新时,需要先将原本数据取出,再进行存储.
set_socket(Name, Socket, #state{datalist = DataList} = State) ->
NewDataList = case lists:keyfind(Name, #user.username, DataList) of
#user{} = User ->
lists:keystore(Name, #user.username, DataList, User#user{socket = Socket});
_ ->
DataList
end,
NewState = State#state{datalist = NewDataList},
{ok, NewState}.
%数据查找
get_from_name(Name, #state{datalist = DataList} = State) ->
lists:keyfind(Name, #user.username, DataList).
%对state中的某一个数据值进行遍历提取
get_socket(#state{datalist=DataList}=State) ->
Socket=[User#user.socket || User <- DataList].
在这一版本中,对原来的结构进行了功能模块的拆分,添加了用户创建,房间搜索等功能,当然其中最为重要的还是对state的操作,因为代码量较大,故将源码分享在github上,有兴趣的可以自行浏览.