1. 程式人生 > >Erlang聊天室功能實現

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

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上,有興趣的可以自行瀏覽.