为了巩固这几天学习erlang的成果,决定找个实例来练练手,在网上找了很多资料,
感觉都是比较零散,要么就是不怎么全面,今天就我花点心思整理一个从0到1的实例出来,当然,先提出,这不是百分百我的原创,只是收集整理再加工而已。
今天所在做的是一个比较经典的聊天室程序,其中用到的知识点有 gen_server的使用,SOCKET编程,以及进程间传送消息等等。
我们先来弄一个echatServer.erl :服务器端程序启动模块。
%%%-------------------------------------------------------------------
%%% @author Administrator
%%% @copyright (C) 2021, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 01. 8月 2021 11:02
%%%-------------------------------------------------------------------
-module(echatServer).
-author("Administrator").
-export([start/0]).
start()->
chat_room:start_link(),%% chat_room 聊天室模块,一个gen_server负责处理客户端请求,保存了所有客户端的连接信息。
chat_acceptor:start(3307),%%chat_acceptor 监听模块
ok.
先定义主模块,再带上一个聊天室模块 负责处理客户端请求,再带上一个监听模块,主要是负责监听从客服端传过来的消息。
我们再来看一下 主体文件 chat_room.erl的内容
%%%-------------------------------------------------------------------
%%% @author Administrator
%%% @copyright (C) 2021, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 01. 8月 2021 11:08
%%%-------------------------------------------------------------------
-module(chat_room).
-author("Administrator").
-behaviour(gen_server).%%行为模式 相当于模板 后面要实现他提供的接口
-include("clientinfo.hrl").
-include("message.hrl").
-record(state,{}).%%定义一个record 名字叫 state
%%导出函数
-export([start_link/0,init/1,getPid/0,bindPid/2,broadCastMsg/1,logout/1]).
-export([handle_call/3,handle_info/2,handle_cast/2,code_change/3,terminate/2]).
%%
%% API Functions
%%
start_link()->
gen_server:start_link({local,?MODULE}, ?MODULE, [],[]).%%用来启动一个gen_server服务器进程
init([])->
id_generator:start_link(),%%生成唯一ID
ets:new(clientinfo,[public,%%保存一些信息到数据库中
ordered_set,
named_table,
{keypos,#clientinfo.id}
]),
{ok,#state{}}
.
handle_call({getpid,Id},From,State)->%%handle_call处理显式调用gen_server:call产生的消息
{ok,Pid}= client_session:start_link(Id),
{reply,Pid,State};
handle_call({remove_clientinfo,Ref},From,State)->
Key=Ref#clientinfo.id,
ets:delete(clientinfo, Key)
;
handle_call({sendmsg,Msg},From,State)->
Key=ets:first(clientinfo),
io:format("feching talbe key is ~p~n",[Key]),
sendMsg(Key,Msg),
{reply,ok,State}
.
%%process messages
%%handle_info这个回调函数被用来处理发给服务器的自发性消息,比如别的进程通过!发来的消息,
又或者是exit信号,一切未显式调用gen_server:call和gen_server:cast的消息都会到handle_info里
handle_info(Request,State)->
{noreply,State}.
%%handle_cast处理显式调用gen_server:cast所产生的消息
是异步的(不关心返回值)。
handle_cast(_From,State)->
{noreply,State}.
terminate(_Reason,_State)->
ok.
code_change(_OldVersion,State,Ext)->
{ok,State}.
getPid()->
Id= id_generator:getnewid(client),
Pid=gen_server:call(?MODULE,{getpid,Id}),
io:format("id generated ~w~n",[Id]),
#clientinfo{id=Id,pid=Pid}.%%给clientinfo赋值
bindPid(Record,Socket)->%%绑定Pid
io:format("binding socket...~n"),
case gen_tcp:controlling_process(Socket, Record#clientinfo.pid) of
{error,Reason}->
io:format("binding socket...error~n");
ok ->
NewRec =#clientinfo{id=Record#clientinfo.id,socket=Socket,pid=Record#clientinfo.pid},
io:format("chat_room:insert record ~p~n",[NewRec]),
ets:insert(clientinfo, NewRec),
Pid=Record#clientinfo.pid,
Pid!{bind,Socket},
io:format("clientBinded~n")
end
.
generatename()->
ok.
broadCastMsg(Msg)->%%广播消息
gen_server:call(?MODULE, {sendmsg,Msg}).
sendMsg(Key,Msg)->%%发送消息
case ets:lookup(clientinfo, Key)of
[Record]->
io:format("Record found ~p~n",[Record]),
Pid=Record#clientinfo.pid,%%获取Pid
io:format("send smg to client_session ~p~n",[Pid]),
Pid!{dwmsg,Msg},
Next=ets:next(clientinfo, Key),
sendMsg(Next,Msg);
[]->
io:format("no clientinfo found~n")
end ,
ok;
sendMsg([],Msg)->
ok.
getMembers(From)->
ok.
setInfo(ClientInfo,From)->
ok.
logout(Ref)->%%退出处理
gen_server:call(?MODULE, {remove_clientinfo,Ref}),
ok.
%%%-------------------------------------------------------------------
%%% @author Administrator
%%% @copyright (C) 2021, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 01. 8月 2021 11:21
%%%-------------------------------------------------------------------
-module(id_generator).
-author("Administrator").
-behavior(gen_server).
-export([start_link/0,getnewid/1]).
-export([init/1,handle_call/3,handle_cast/2,handle_info/2,terminate/2,code_change/3]).
-record(ids,{idtype,ids}).
-record(state,{}).
start_link()->
gen_server:start_link({local,?MODULE}, ?MODULE, [],[])
init([])->
mnesia:start(),
io:format("Started"),
mnesia:create_schema([node()]),
case mnesia:create_table(ids,[{type,ordered_set},
{attributes,record_info(fields,ids)},
{disc_copies,[]}
]) of
{atomic,ok}->
{atomic,ok};
{error,Reason}->
io:format("create table error")
end,
{ok,#state{}}.
getnewid(IdType)->
mnesia:force_load_table(ids),
gen_server:call(?MODULE, {getid,IdType})
.
%%生成新的ID 并 获取type
handle_call({getid,IdType},From,State)->
F=fun()->
Result=mnesia:read(ids,IdType,write),
case Result of
[S]->
Id=S#ids.ids,
NewClumn=S#ids{ids=Id+1},
mnesia:write(ids,NewClumn,write),
Id;
[]->
NewClumn=#ids{idtype=IdType,ids=2},
mnesia:write(ids,NewClumn,write),
1
end
end,
case mnesia:transaction(F)of
{atomic,Id}->
{atomic,Id};
{aborted,Reason}->
io:format("run transaction error ~1000.p ~n",[Reason]),
Id=0;
_Els->
Id=1000
end,
{reply,Id,State}
%%处理接收到的信息
handle_cast(_From,State)->
{noreply,ok}.
handle_info(Request,State)->
{noreply,ok}.
terminate(_From,State)->
ok.
code_change(_OldVer,State,Ext)->
{ok,State}.
id_generator.erl这个文件主要是保存数据用的,也是实现了gen_server 行为模式,可以进行进程间数据传递,
%%%-------------------------------------------------------------------
%%% @author Administrator
%%% @copyright (C) 2021, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 01. 8月 2021 11:22
%%%-------------------------------------------------------------------
-module(client_session).
-author("Administrator").
-behavior(gen_server).
-include("clientinfo.hrl").
-include("message.hrl").
-export([init/1,start_link/1,handle_info/2,handle_call/3,terminate/2]).
-export([process_msg/1]).
start_link(Id)->
gen_server:start_link(?MODULE, [Id], []).
init(Id)->
State=#clientinfo{id=Id},
{ok,State}.
%%处理从房间来的消息 也就是从客户端发过来的消息
handle_info({dwmsg,Message},State)->
io:format("client_session dwmsg recived ~p~n",[Message]),
case gen_tcp:send(State#clientinfo.socket, Message#message.content)of
ok->
io:format("client_session dwmsg sended ~n");
{error,Reason}->
io:format("client_session dwmsg sended error ~p ~n",Reason)
end,
{noreply,State};
handle_info({bind,Socket},State)->
io:format("client_session bind socket ~n"),
NewState=State#clientinfo{socket=Socket},
io:format("NewState ~p~n",[NewState]),
{noreply,NewState};
handle_info({tcp,Socket,Data},State)->
io:format("client_session tcp data recived ~p~n",[Data]),
%io:format("msg recived ~p~n",[Message]),
NewMsg=#message{type=msg,from=State#clientinfo.id,content=Data},
chat_room:broadCastMsg(NewMsg),
{noreply,State};
handle_info({tcp_closed,Socket},State)->
chat_room:logout(State);
handle_info(stop,State)->
io:format("client stop"),
{stop,normal,stopped,State};
handle_info(Request,State)->
io:format("client_session handle else ~p~n",[Request]),
{noreply,State}.
handle_call(Request,From,State)->
{reply,ok,State}.
handle_cast(Request,State)->
{noreply,State}.
terminate(_Reason,State)->
ok.
process_msg(State)->
io:format("client_session:process_msg SOCKET:~p ~n",[State#clientinfo.socket]),
case gen_tcp:recv(State#clientinfo.socket, 0) of
{ok,Message}->
io:format("recived ~p ~n",[Message]),
%io:format("msg recived ~p~n",[Message]),
NewMsg=#message{type=msg,from=State#clientinfo.id,content=Message},
chat_room:broadCastMsg(NewMsg),
process_msg(State);
{error,closed}->
io:format("client_session:recive error ~n"),
process_msg(State);
Any->
io:format("client_session:recive any ~n"),
process_msg(State)
client_session.erl 主要是处理从客户端发过来的消息,并做了相应的回应,
%%%-------------------------------------------------------------------
%%% @author Administrator
%%% @copyright (C) 2021, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 01. 8月 2021 11:24
%%%-------------------------------------------------------------------
-module(chat_acceptor).
-author("Administrator").
-export([start/1,accept_loop/1]).
start(Port)->
case (do_init(Port))of
{ok,ListenSocket}->
accept_loop(ListenSocket);
_Els ->
error
end.
%%监听端口
do_init(Port) when is_list(Port)->
start(list_to_integer(Port));
do_init([Port]) when is_atom(Port)->
start(list_to_integer(atom_to_list(Port)));
do_init(Port) when is_integer(Port)->
Options=[binary,
{packet, 0},
{reuseaddr, true},
{backlog, 1024},
{active, true}],
case gen_tcp:listen(Port, Options) of
{ok,ListenSocket}->
{ok,ListenSocket};
{error,Reason} ->
{error,Reason}
end.
%%接收消息
accept_loop(ListenSocket)->
case (gen_tcp:accept(ListenSocket, 3000))of
{ok,Socket} ->
process_clientSocket(Socket),
?MODULE:accept_loop(ListenSocket);
{error,Reason} ->
?MODULE:accept_loop(ListenSocket);
{exit,Reason}->
?MODULE:accept_loop(ListenSocket)
end.
%%客户端进程处理函数
process_clientSocket(Socket)->
Record=chat_room:getPid(),
chat_room:bindPid(Record, Socket),
ok.
上面的一个chat_accpetor.erl主是要绑定监听端口并进行消息转发。到些,整个服务端流程基本搞清了,开始启动两个进程,一个主进程chat_room处理各种初始化操作,并进行消息发送,广播,可以理解成一个中转端,进程的各种消息和客户端传过来的消息在这里进行分发,另一个 chat_accpetor 负责监听并转发消息给 chat_room中进行处理。
下面,我们弄一个客户端进行测试,我们用eclipse来做一个安卓APP,
然后连接测试一下,
发送的消息,222 服务端己收到,说明客户端跟服务器己能正常通讯,OK,这篇文章先到,后续我们继续更新 。