菜鸟一枚,老大说要做个socket聊天室,用socket实现,所以就网上一搜基本都没什么资料,这里贡献一篇我自己写的很简陋的多人聊天室,有不对的地方,多多指出,谢谢。
- 基本要求:
1. 写一个支持多人在线的socket聊天服务器和客户端,需要实现以下功能:
a. 用户登录,用户管理相关的功能无需实现,手工构造几个用户数据就可以了;
b. 实现用户登录,聊天和退出功能。用户数据中需要记录登录次数和聊天次数;
c. 能够统计当前在线用户数;
d. 用户数据存储在ets中;
e. 定义服务端和客户的端通讯协议;
%% 用户数据结构:
-record(user, {
id %% 用户ID
,name %% 用户名称
,passwd %% 用户登录密码
,login_times %% 登录次数
,chat_times %% 聊天次数
,last_login %% 最后一次登录时间
,state %% 登录状态 0 未登录 1已登录
}
).
服务端代码:
-module(server_example). %-export([start/0,initialize_ets/0,info_lookup/1,loop/2,info_update/3,init/1,handle_call/3,terminate/2]). %-import(counter,[start/1,add/1,value/1,decrease/1,log_add/1,chat_add/1]). %-import(my_fsm,[start_count/0,count/1]). -compile(export_all). -include("user_info.hrl"). -define(SERVER,?MODULE). start() -> gen_server:start_link({local,?SERVER},?MODULE,[],[]). init([]) -> initialize_ets(), start_parallel_server(), {ok,ets:new(mysocket,[public,named_table])}. %开启服务器 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), 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}), ets:insert(test,#user{id=02,name="qiqi",passwd="123",login_times=0,chat_times=0,last_login={},state=0}), ets:insert(test,#user{id=03,name="cym",passwd="123",login_times=0,chat_times=0,last_login={},state=0}). %查询ets info_lookup(Key) -> %返回值是一个元组 ets:lookup(test,Key). %修改ets信息 info_update(Key,Pos,Update) -> ets:update_element(test,Key,{Pos,Update}). %<<-------------------------回调函数----------------------------->> %登录的时候添加socket handle_call({addSocket,UserName,Socket},_From,Tab) -> Reply = case ets:lookup(Tab,UserName) of [{UserName,Socket}] -> have_socket; [] -> ets:insert(Tab,{UserName,Socket}) end, %这里是顺序结构,所以用逗号 io:format("Tab ~p~n",[Tab]), % io:format("mysocket ~p~n",[ets:i(Tab)]), {reply,Reply,Tab}; %退出的时候,删除socket handle_call({deleteSocket,UserName,Socket},_From,Tab) -> Reply = case ets:lookup(Tab,UserName) of [{UserName,Socket}] -> ets:delete(Tab,UserName); [] -> io:format("no exist this socket ~p~n",[Socket]) end, %这里是顺序结构,所以用逗号 % io:format("mysocket ~p~n",[ets:i(Tab)]), {reply,Reply,Tab}; %用户在线个数 handle_call({userNumber},_From,Tab) -> Socketlist = ets:tab2list(Tab), io:format("~p user online~n",[length(Socketlist)]), {reply,[],Tab}; %广播信息 handle_call({sendAllMessage,Name,Msg},_From,Tab) -> %Socketlist = [{UserName,Socket,Name,Msg}||{UserName,Socket} <- ets:tab2list(Tab)], 不用推导了 Socketlist =[{UserName,Socket}||{UserName,Socket} <- ets:tab2list(Tab),UserName =/= Name], io:format("list ~p~n",[Socketlist]), lists:foreach( fun({UserName,Socket}) -> N = term_to_binary(Name), %用户名字长度 竟然可以用外面的变量 M = term_to_binary(Msg), %信息长度 Packet = <<0010:16,(byte_size(N)):16,N/binary,(byte_size(M)):16,M/binary>>, gen_tcp:send(Socket,Packet) end, Socketlist ), {reply,[],Tab}. %<<-------------------------回调函数----------------------------->> %用户在线人数。 userNumber() -> gen_server:call(server_example,{userNumber}). %接收信息并处理 loop(Socket) -> %loop(Pid,Socket) io:format("receiving...~n"), receive %Msg -> io:format(" ~p~n",[Msg]); {tcp,Socket,Bin} -> io:format("i carlos am coming~n"), %<<State:16,Str1:2/binary,Str2:2/binary>> = Bin, <<State:16,Date/binary>> = Bin, <<Size1:16,Date1/binary>> = Date, %姓名的长度 <<Str1:Size1/binary,Date2/binary>> = Date1, <<Size2:16,Date3/binary>> = Date2, %密码的长度 <<Str2:Size2/binary,Date4/binary>> = Date3, %io:format(" log2 ~p ~p ~n",[Str1,Str2]), %case binary_to_term(Bin) of case State of %登录 0000 -> %{Name,Passwd} = binary_to_term(Str), Name = binary_to_term(Str1), io:format("logining ~p ~n",[Name]), %cym case info_lookup(Name) of [{user,Uid,Pname,Pwd,Logc,ChatC,Lastlog,LonginState}] -> S = term_to_binary("success"), N = term_to_binary(Name), Packet = <<0000:16,(byte_size(S)):16,S/binary,(byte_size(N)):16,N/binary>>, % Packet = <<0000:16>>, %处理一下业务, 登录次数加1 状态改为 1,登录时间在退出的时候才修改, %mysocket如果还没有添加socket就添加一下socket gen_server:call(server_example,{addSocket,Pname,Socket}), info_update(Pname,5,Logc+1), info_update(Pname,8,1), io:format("after logining ~p~n",[info_lookup(Pname)]), gen_tcp:send(Socket,Packet), io:format("user ~p have logged~n",[Name]), % io:format("user ~p have logged ~p times ~n",[Name,Reply1]), loop(Socket); %loop(Pid,Socket); %为空表示该用户没有记录 [] -> io:format("you haved not registered yet"), %返回的是[] 而不是 [{}] F = term_to_binary("failed"), N = term_to_binary(Name), Packet = <<0000:16,(byte_size(F)):16,F/binary,(byte_size(N)):16,N/binary>>, gen_tcp:send(Socket,Packet), loop(Socket) % loop(Pid,Socket) end; %接收信息 0001 -> Name = binary_to_term(Str1), Msg = binary_to_term(Str2), [#user{chat_times=Ccount,state=LoginState}] = info_lookup(Name), %更新聊天次数 case LoginState of 1 -> info_update(Name,6,Ccount+1), % Reply = personal_chat_count({cadd,Name,Ccount}), % io:format("User ~p :~p~n",[Name,Msg]), % io:format("User ~p have chatted with his friend on line ~p times ~n",[Name,Reply]), N = term_to_binary({"ok","received"}), Len = byte_size(N), Packet = <<0001:16,Len:16,N/binary>>, io:format("received the Msg ~ts : ~ts~n",[Name,Msg]), %广播信息 gen_tcp:send(Socket,Packet), gen_server:call(server_example,{sendAllMessage,Name,Msg}), loop(Socket); %loop(Pid,Socket); 0 -> N = term_to_binary({"failed","noLogin"}), Len = byte_size(N), Packet = <<0001:16,Len:16,N/binary>>, io:format("user ~p no login",[Name]), gen_tcp:send(Socket,Packet), loop(Socket) %loop(Pid,Socket); end; %退出 0002 -> Name = binary_to_term(Str2), io:format("see ~p: ~p~n",[Name,info_lookup(Name)]), [#user{login_times=Log,last_login=LastLo}] = info_lookup(Name), Last = calendar:now_to_local_time(erlang:now()), % 4.格式化时间 todo % mysocket里,去除这个socket。 gen_server:call(server_example,{deleteSocket,Name,Socket}), N = term_to_binary("ok"), Packet = <<0002:16,(byte_size(N)):16,N/binary>>, gen_tcp:send(Socket,Packet), %修改最后登录时间 info_update(Name,7,Last), info_update(Name,8,0), io:format("after logout ~p~n",[info_lookup(Name)]) %io:format("~p users online~n",[Reply]) end; {tcp_closed,Socket} -> io:format("Server socket closed~n") end.
客户端代码:
-module(client_example). -define(SERVER,?MODULE). -compile(export_all). %-export([get_socket/0,client_login/1,send_message/1,logout/1]). start() -> gen_server:start_link({local,?SERVER},?MODULE,[],[]). %%初始化 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)). %登录接口 login(Name,Password) -> % io:format("log1 ~p ~p ~n ",[Name,Password]), client ! {self(),{login,Name,Password}}, receive Response -> Response end. %<<-------------------------回调函数----------------------------->> %登录的时候添加socket handle_cast({addSocket,UserName,Socket},Tab) -> case ets:lookup(Tab,UserName) of [{UserName,Socket}] -> have_socket; [] -> ets:insert(Tab,{UserName,Socket}) end, %这里是顺序结构,所以用逗号 io:format("Tab ~p~n",[Tab]), % io:format("mysocket ~p~n",[ets:i(Tab)]), {noreply,Tab}; %退出的时候,删除socket handle_cast({deleteSocket,UserName,Socket},Tab) -> case ets:lookup(Tab,UserName) of [{UserName,Socket}] -> ets:delete(Tab,UserName); [] -> io:format("no exist this socket ~p~n",[Socket]) end, %这里是顺序结构,所以用逗号 % io:format("mysocket ~p~n",[ets:i(Tab)]), {noreply,Tab}. %<<-------------------------回调函数----------------------------->> %聊天发送接口 send_message(Msg) -> %io:format("i want send Msg"), client ! {self(),{msg,Msg}}, receive Response -> Response end. %退出接口 logout(Name) -> client ! {self(),{logout,Name}}, 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>>, % Packet = <<1:16>>, %io:format(Packet), gen_tcp:send(Socket,Packet ), % io:format("before receive server ~n"), % gen_tcp:send(Socket,term_to_binary("hello")), receive %来自服务端socket响应 % Msg -> io:format("coming ~p ~n",[Msg]); {tcp,Socket,Bin} -> %<<State:4,S:2/binary,N:2/binary>> = Bin, <<State:16,Date/binary>> = Bin, %状态码 <<Size1:16,Date1/binary>> = Date, %登录成功信息长度 <<S:Size1/binary,Date2/binary>> = Date1, %登录成功信息 <<Size2:16,Date3/binary>> = Date2, %用户名的长度 <<N:Size2/binary,Date4/binary>> = Date3, %用户名 %io:format("long2 ~n"), case binary_to_term(S) of "success" -> %io:format("long3 ~n"), gen_server:cast(?SERVER,{addSocket,Name,Socket}), From ! {"you have login successfully"}; % io:format("you have login successfully ~n"); "failed" -> %io:format("long4 ~n"), From ! {"you haved login failed,please try again"}, gen_tcp:close(Socket) end after 5000 -> io:format("overTime1") end, handle(Socket); %发送信息协议号0001 {msg,Msg} -> %io:format("my message:~p~n",[Msg]), case ets:match(mysocket, {'$1', Socket}) of [[Name]] -> N = term_to_binary(Name); Name -> N = term_to_binary("noLogin") end, % [[Name]] = ets:match(mysocket, {'$1', Socket}), io:format("user:~p my message:~p~n",[Name,Msg]), % N = term_to_binary(Name), 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,N:2/binary>> = Bin, <<State:16,Date/binary>> = Bin, %状态码 <<Size1:16,Date1/binary>> = Date, %消息长度 ReceiveMsg = binary_to_term(Date1), % io:format("State : ~p,Size1: ~p ~p~n ",[State,Size1,ReceiveMsg]), %<<N:Size1/binary,Date2/binary>> = Date1, %发送消息 的反馈消息 为什么匹配会失败 %case binary_to_term(N) of case ReceiveMsg of {"ok","received"} -> From ! {"ok"}; {"failed","noLogin"} -> From ! {"you don't have logined ~n"}; Fail -> io:format(" ~p~n",[ReceiveMsg]), From ! {"failed ~n"} end after 3000 -> io:format("overTime2") end, handle(Socket); %登出协议号0002 {logout,Name} -> L = term_to_binary("logout"), N = term_to_binary(Name), Packet = <<0002:16,(byte_size(L)):16,L/binary,(byte_size(N)):16,N/binary>>, gen_server:cast(?SERVER,{deleteSocket,Name,Socket}), gen_tcp:send(Socket,Packet), receive {tcp,Socket,Bin} -> <<State:16,Date/binary>> = Bin, %状态码 <<Size1:16,Date1/binary>> = Date, %消息长度 ReceiveMsg = binary_to_term(Date1), % io:format("ReceiveMsg :~p~n",[ReceiveMsg]), case ReceiveMsg of "ok" -> From ! {"ok,you will logout successfully ~n"} end after 5000 -> ok end, gen_tcp:close(Socket) % 关闭socket时只有再次连接新socket才能打开 end; {tcp,Socket,Bin} -> %io:format("other user message~n"), <<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) end.
头文件
-record (user,{id=00,name,passwd,login_times,chat_times,last_login={},state=0}).
备注: 当然其中会有一些bug,本人因为懒得原因后面也没有去修复,但是还是可以用一下的~~,希望对初学者有帮助