优秀的编程知识分享平台

网站首页 > 技术文章 正文

erlang编程实战篇之网络通信编程(一)

nanyue 2024-07-31 12:07:08 技术文章 11 ℃

为了巩固这几天学习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,这篇文章先到,后续我们继续更新 。

最近发表
标签列表