Erlang聊天室功能实现

作为新手的练习项目,使用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上,有兴趣的可以自行浏览.

猜你喜欢

转载自blog.csdn.net/qq_34755443/article/details/85019875