Erlang聊天室功能實現
阿新 • • 發佈:2018-12-16
作為新手的練習專案,使用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上,有興趣的可以自行瀏覽.