erlang,socket简单实现多人聊天室

        菜鸟一枚,老大说要做个socket聊天室,用socket实现,所以就网上一搜基本都没什么资料,这里贡献一篇我自己写的很简陋的多人聊天室,有不对的地方,多多指出,谢谢。

  1.  基本要求:

   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,本人因为懒得原因后面也没有去修复,但是还是可以用一下的~~,希望对初学者有帮助


扫描二维码关注公众号,回复: 815703 查看本文章

猜你喜欢

转载自blog.csdn.net/qq_23301369/article/details/80288955