文章目录
注意:erlang的进程式编程,用shell脚本方便管理,应使用linux来学习较好,本项目还可以继续在window环境下执行
1. 项目结构
2.学会编译erl文件至指定目录
Emakefile 文件,执行方法,在其所在的目录下打开erl终端,输入make :all() 即可编译,内容如下:
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% 聊天工程-处理运行命令,使用shell脚本方便管理整个工程,里面多个小模块直接使用Emakefile更加方便
%% c(模块名) 单独
%% make :all(). 使用Emakefile
%% emakefile_hd.erl Emakefile编译时处理需要复制的文件,通过erl编译文件实现
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
{"emakefile_do.erl", [debug_info, {i,"include"}, {outdir,"."}]}.
{"src/socket_chat/*", [debug_info, {i,"include"}, {outdir,"ebin/socket_chat"}]}.
{"src/otp_chat/*", [debug_info, {i,"include"}, {outdir,"ebin/otp_chat"}]}.
{"src/websocket_otp_chat/*", [debug_info, {i,"include"}, {outdir,"ebin/websocket_otp_chat"}]}.
3.编译文件目录如图
4. 如何运行,编译输出后,到底编译的文件夹目录,打开erl终端,然后就可以执行模块的方法了,如这样就启动聊天服务端了
5.项目代码说明
1.原生socket连接聊天系统
tcp聊天
tcp_server.erl 服务端
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @doc
%%% tcp 服务端
%%% @end
%%% Created : 2023/8/9 19:32
%%%-------------------------------------------------------------------
-module(tcp_server).
%% API
-export([listen_socket/1, start/0]).
-define(SERVER_PORT, 8080).
start() ->
listen_socket(?SERVER_PORT),
ok.
%% 监听连接客户端
listen_socket(ServerPort) ->
{ok, ListenSocket} = gen_tcp:listen(ServerPort, [{active, false}, {reuseaddr, true}]),
error_logger:info_msg("tcp 服务端 启动成功"),
accept_connections(ListenSocket).
%%处理客户端连接
accept_connections(ListenSocket) ->
accept_connections(ListenSocket, []).
accept_connections(ListenSocket, Clients) ->
{ok, ClientSocket} = gen_tcp:accept(ListenSocket),
error_logger:info_msg("accept_connections:~w~n", [ClientSocket]),
NewClients = [{ClientSocket, undefined} | Clients],
spawn(fun() -> handle_client(ClientSocket, NewClients) end),
accept_connections(ListenSocket, NewClients).
%%处理客户端发送的消息
handle_client(ClientSocket, Clients) ->
{ok, SocketData} = gen_tcp:recv(ClientSocket, 0),
Res = iolist_to_binary(SocketData),
Result = binary_to_term(Res),
case Result of
{ok, Msg} = MsgData ->
io:format("收到消息:socket:~w, msg:~ts~n", [ClientSocket, Msg]),
broadcast_message(Clients, MsgData);
Error ->
error_logger:info_msg("消息格式错误:~w", [Error])
end,
handle_client(ClientSocket, Clients).
%%广播消息-支持多人聊天
broadcast_message(Clients, Message) ->
lists:foreach(fun({ClientSocket, _}) ->
gen_tcp:send(ClientSocket, term_to_binary(Message))
end, Clients).
tcp_client.erl 客户端
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @doc
%%% tcp 客户端
%%% @end
%%% Created : 2023/8/15 18:54
%%%-------------------------------------------------------------------
-module(tcp_client).
%% API
-export([start/0]).
-define(SERVER_IP, "127.0.0.1").
-define(SERVER_PORT, 8080).
start() ->
{ok, Socket} = gen_tcp:connect(?SERVER_IP, ?SERVER_PORT, [binary, {packet, 0}]),
error_logger:info_msg("tcp客户端 连接成功"),
spawn(fun() -> loop_input(Socket) end),
receive_response(Socket).
loop_input(Socket) ->
MsgData = {ok, string:trim(io:get_line("请输入你的消息: "))},
gen_tcp:send(Socket, term_to_binary(MsgData)),
loop_input(Socket).
receive_response(Socket) ->
receive
{tcp, Socket, Bin} ->
case binary_to_term(Bin) of
{ok, Msg} ->
io:format("(收到聊天消息):~ts~n", [Msg]);
Error1 ->
error_logger:info_msg("Error receiving response: ~w~n", [Error1])
end;
Error2 ->
error_logger:info_msg("Error receiving response: ~w~n", [Error2])
end,
receive_response(Socket).
udp聊天
udp_server.erl 服务端
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @doc
%%% udp 服务端
%%% @end
%%% Created : 2023/8/9 19:32
%%%-------------------------------------------------------------------
-module(udp_server).
%% API
-export([start/0]).
-define(SERVER_IP, "127.0.0.1").
-define(SERVER_PORT, 8001).
start() ->
{ok, ListenSocket} = gen_udp:open(?SERVER_PORT, [binary]),
error_logger:info_msg("udp 服务端 启动成功"),
receive_response(ListenSocket).
%%处理客户端连接
receive_response(Socket) ->
receive
{udp, Socket, Host, Port, Bin} ->
case binary_to_term(Bin) of
{ok, Msg} = MsgData ->
io:format("收到消息:~ts~n", [Msg]),
gen_udp:send(Socket, Host, Port, term_to_binary(MsgData)),
receive_response(Socket);
Error1 ->
error_logger:info_msg("Error receiving response: ~w~n", [Error1])
end;
Error2 ->
error_logger:info_msg("Error receiving response: ~w~n", [Error2])
end,
receive_response(Socket).
udp_client.erl 客户端
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @doc
%%% udp 客户端
%%% @end
%%% Created : 2023/8/15 18:54
%%%-------------------------------------------------------------------
-module(udp_client).
%% API
-export([start/0]).
-define(SERVER_IP, "localhost").
-define(SERVER_PORT, 8001).
start() ->
{ok, Socket} = gen_udp:open(0, [binary]),
error_logger:info_msg("udp客户端 连接成功"),
spawn(fun() -> loop_input(Socket) end),
receive_response(Socket).
loop_input(Socket) ->
MsgData = {ok, string:trim(io:get_line("请输入你的消息: "))},
gen_udp:send(Socket, ?SERVER_IP, ?SERVER_PORT, term_to_binary(MsgData)),
loop_input(Socket).
receive_response(Socket) ->
receive
{udp, Socket, _Host, _Port, Bin} ->
case binary_to_term(Bin) of
{ok, Msg} ->
io:format("(收到聊天消息):~ts~n", [Msg]);
Error1 ->
error_logger:info_msg("Error receiving response: ~w~n", [Error1])
end;
Error2 ->
error_logger:info_msg("Error receiving response: ~w~n", [Error2])
end,
receive_response(Socket).
2.基于Otp框架搭建tcp服务端进行聊天,客户端其实同理,先自行熟悉OTP框架内容
chat.app 应用层 应用程序描述文件
在Erlang中,chat.app
文件通常是一个应用程序描述文件(.app 文件)。这个文件包含了有关 Erlang 应用程序的元数据,例如应用程序的名称、版本号、模块列表以及其他相关信息。
{application, chat,
[{description, "rfos - chat server!"},
{id, "chat"},
{vsn, "0.1"},
{modules, [chat]},
{registered, [chat, chat_server_sup]},
{applications, [kernel, stdlib, sasl]},
{mod, {chat, []}},
{env, []}
]}.
chat.hrl 一些普通宏定义文件
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2024,
%%% @doc
%%%
%%% @end
%%% Created : 02. 四月 2024 15:01
%%%-------------------------------------------------------------------
%%日志封装
-define(INFO(Format), error_logger:info_msg(Format, [])).
-define(INFO(Format, Args), error_logger:info_msg(Format, Args)).
-define(SERVER_IP, "127.0.0.1").
-define(SERVER_PORT, 8080).
-record(state, {socket, clients}).
-define(chat_client_list, chat_client_list). % 进程字典名称,用于存储各个聊天接口的连接,广播消息的需要[{ConnectSocket}]
-define(ROLE_PROCESS_EXIT_WAIT, 10).%%角色进程退出时等待多少秒
common_util.erl 普通工具类
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2010, rfos
%%% @doc
%%% 普通类 控制
%%% @end
%%%-------------------------------------------------------------------
-module(common_util).
-include("chat.hrl").
-export([manage_applications/6, start_applications/1, stop_applications/1, copy_files/3]).
manage_applications(Iterate, Do, Undo, SkipError, ErrorTag, Apps) ->
Iterate(fun(App, Acc) ->
case Do(App) of
ok ->
?INFO("manage_applications:~ts~ts", [App, "操作成功!"]),
[App | Acc];
{error, {SkipError, _}} -> Acc;
{error, Reason} ->
lists:foreach(Undo, Acc),
throw({error, {ErrorTag, App, Reason}})
end
end, [], Apps),
?INFO("app stop success"),
ok.
start_applications(Apps) ->
manage_applications(fun lists:foldl/3, fun application:start/1, fun application:stop/1, already_started, cannot_start_application, Apps).
stop_applications(Apps) ->
manage_applications(fun lists:foldr/3, fun application:stop/1, fun application:start/1, not_started, cannot_stop_application, Apps).
%% 文件复制,启动App时拷贝需要的文件
copy_files(SourceDir, DestDir, Extension) ->
FileList = filelib:wildcard(filename:join(SourceDir, "*" ++ Extension)),
io:format("TaegetFile:~p , FileList:~p ~n", [filename:join(SourceDir, "*" ++ Extension), FileList]),
lists:foreach(fun(File) ->
DestFile = filename:join(DestDir, filename:basename(File)),
file:copy(File, DestFile)
end, FileList).
chat.erl 应用程序文件
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2024,
%%% @doc
%%%
%%% @end
%%% Created : 02. 四月 2024 15:01
%%%-------------------------------------------------------------------
-module(chat).
-behaviour(application).
-include("chat.hrl").
-export([
start/2,
stop/1,
start/0,
stop/0
]).
-define(APPS, [sasl, chat]).
%% --------------------------------------------------------------------
start() ->
%% 拷贝需要的文件至编译目录下,并启动App文件
%% 拷贝需要的文件至编译目录下,并启动App文件,
{ok, DoPath} = file:get_cwd(),
?INFO("等待文件拷贝......,DoPath=~ts", [DoPath]),
common_util:copy_files(DoPath ++ "/../../src/otp_chat", DoPath ++ "/../../ebin/otp_chat", ".app"),
common_util:copy_files(DoPath ++ "/../../src/otp_chat", DoPath ++ "/../../ebin/otp_chat", ".hrl"),
timer:sleep(timer:seconds(2)),
ok = common_util:start_applications(?APPS).
stop() ->
ok = common_util:stop_applications(?APPS).
%% --------------------------------------------------------------------
start(normal, []) ->
{ok, SupPid} = chat_server_sup:start_link(),
lists:foreach(
fun({Msg, Func}) ->
?INFO("starting ~ts ...", [Msg]),
Func()
end, get_start_fun_list()),
?INFO("~w ~ts~n", [?MODULE, "启动成功"]),
{ok, SupPid}.
%% --------------------------------------------------------------------
stop(_State) ->
ok.
%% get_start_fun_list
get_start_fun_list() ->
[
{
"init manager info",
fun() ->
chat_server:start()
end
},
{"Start Actor Supverisor",
fun() ->
chat_client_sup:start()
end
}
].
chat_server_sup.erl 监督树 应用监控
-module(chat_server_sup).
-behaviour(supervisor).
-include("chat.hrl").
-export([start_link/0]).
-export([init/1]).
-define(SERVER, ?MODULE).
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
init([]) ->
{ok, {{one_for_one, 10, 10}, []}}.
common_supervisor.erl 另建一个erl文件用于创建子监督树
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2023,
%%% @doc
%%% 创建子监督树
%%% @end
%%%-------------------------------------------------------------------
-module(common_supervisor).
-export([start_child/2, start_child/3, start_supervisor/2]).
start_child(SupName, Module) ->
start_child(SupName, Module, 10 * 1000).
start_child(SupName, Module, ShutdownTimeout) ->
supervisor:start_child(SupName, {Module, {Module, start_link, []}, permanent, ShutdownTimeout, worker, [Module]}).
start_supervisor(SupName, Module) ->
supervisor:start_child(SupName, {Module, {Module, start_link, []}, permanent, 10 * 1000, supervisor, [Module]}).
chat_server.erl 聊天服务端进程管理
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2024,
%%% @doc
%%% 聊天进程管理
%%% @end
%%% Created : 05. 四月 2024 13:17
%%%-------------------------------------------------------------------
-module(chat_server).
-behavior(gen_server).
-include("chat.hrl").
%% API
-export([init/1, handle_call/3, handle_cast/2, start/0, start_link/0, terminate/2, code_change/3, handle_info/2, pid/0, send/1]).
-export([get_data/0, set_data/1]).
start() ->
{ok, _} = common_supervisor:start_child(chat_server_sup, ?MODULE).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
erlang:process_flag(trap_exit, true),
Opts = [{packet, 0}, {active, false}],
case gen_tcp:listen(?SERVER_PORT, Opts) of
{ok, ListenSocket} ->
?INFO("otp tcp 服务端 启动监听成功"),
send({accept_connect, ListenSocket}),
{ok, #state{socket = ListenSocket}};
{error, Reason} ->
{stop, Reason}
end.
send(Info) ->
pid() ! Info.
handle_call(_Request, _From, _State) ->
?INFO("handle_call:_Request=~w,_From=~w,_State=~w", [_Request, _From, _State]),
erlang:error(not_implemented).
handle_cast(_Request, _State) ->
?INFO("handle_cast:_Request=~w,State=~w", [_Request, _State]),
erlang:error(not_implemented).
%% 处理连接监听端口
handle_info({accept_connect, ListenSocket}, State) ->
%% 此处有另一种方式:prim_inet:async_accept 异步
%% 此处采用普通新建进程方式异步,不然会阻塞
spawn(fun() -> listen_socket(ListenSocket) end),
{noreply, State};
%% 存储数据
handle_info({set_data, ClientSocket}, State) ->
set_data(ClientSocket),
{noreply, State};
%% 广播玩家
handle_info({broadcast, MsgData}, State) ->
ToList = get_data(),
?INFO("broadcast:MsgData=~w,List=~w", [MsgData, ToList]),
lists:foreach(fun(ClientSocket) ->
gen_tcp:send(ClientSocket, term_to_binary(MsgData))
end, get_data()),
{noreply, State};
handle_info(Info, State) ->
?INFO("handle_info:Info=~w,State=~w", [Info, State]),
{noreply, State}.
terminate(_Reason, #state{socket = Socket} = State) ->
?INFO("terminate:_Reason=~w,State=~w", [_Reason, State]),
gen_tcp:close(Socket),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
pid() ->
erlang:whereis(?MODULE).
%% 进程字典操作
get_data() ->
case get(?chat_client_list) of
List when is_list(List) -> List;
_ -> []
end.
set_data(ClientSocket) ->
case get(?chat_client_list) of
List when is_list(List) ->
put(?chat_client_list, [ClientSocket | lists:delete(ClientSocket, List)]);
_ ->
put(?chat_client_list, [ClientSocket])
end.
listen_socket(ListenSocket) ->
{ok, ClientSocket} = gen_tcp:accept(ListenSocket),
{ok, Mod} = inet_db:lookup_socket(ListenSocket),
inet_db:register_socket(ClientSocket, Mod),
%% 将此连接存储起来
chat_server:send({set_data, ClientSocket}),
%% 开启客户端连接聊天子进程
{ok, PID} = supervisor:start_child(chat_client_sup, [{ClientSocket}]),
%% 将socket监听移到该进程
gen_tcp:controlling_process(ClientSocket, PID),
?INFO("accept_connect:{ListenSocket, ClientSocket, PID}=~w~n", [{ListenSocket, ClientSocket, PID}]),
%% 循环发消息监听下个连接
listen_socket(ListenSocket).
chat_client_sup.erl 聊天进程管理-处理玩家消息进程的监督树
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2024,
%%% @doc
%%% 聊天进程管理-处理玩家消息进程的监督树
%%% @end
%%% Created : 05. 四月 2024 13:17
%%%-------------------------------------------------------------------
-module(chat_client_sup).
-include("chat.hrl").
-behavior(supervisor).
-export([start/0, start_link/0, init/1]).
start() ->
{ok, _Pid} = common_supervisor:start_supervisor(chat_server_sup, ?MODULE).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
ChildSpec = {
chat_client_handler,
{chat_client_handler, start_link, []},
temporary,
?ROLE_PROCESS_EXIT_WAIT,
worker,
[chat_client_handler]
},
{ok, {{simple_one_for_one, 1, 1}, [ChildSpec]}}.
chat_client_handler.erl 客户端消息接收处理子进程服务端
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2024,
%%% @doc
%%% 聊天进程管理-处理客户端消息进程
%%% @end
%%% Created : 05. 四月 2024 13:17
%%%-------------------------------------------------------------------
-module(chat_client_handler).
-behavior(gen_server).
-include("chat.hrl").
-export([start_link/1, init/1, pid/0, send/1]).
-export([handle_info/2, handle_cast/2, handle_call/3, terminate/2, code_change/3]).
start_link({ClientSocket}) ->
gen_server:start_link({local, list_to_atom(port_to_list(ClientSocket))}, ?MODULE, [{ClientSocket}], []).
init([{ClientSocket}]) ->
erlang:process_flag(trap_exit, true),
inet:setopts(ClientSocket, [{active, once}, {packet, 0}, binary]),
{ok, {IP, Port}} = inet:peername(ClientSocket),
?INFO("chat_client_handler {ClientSocket, IP, Port}=~w", [{ClientSocket, IP, Port}]),
{ok, #state{socket = ClientSocket}}.
send(Info) ->
pid() ! Info.
handle_call(_Request, _From, _State) ->
erlang:error(not_implemented).
handle_cast(_Request, _State) ->
erlang:error(not_implemented).
%% _Socket = ClientSocket
handle_info({tcp, _Socket, Data}, #state{socket = ClientSocket} = State) ->
inet:setopts(ClientSocket, [{active, once}, {packet, 0}, binary]),
Result = binary_to_term(Data),
case Result of
{ok, Msg} ->
io:format("收到消息:socket:~w, msg:~ts~n", [ClientSocket, Msg]),
%% 如果私聊,则直接发送给玩家即可,触类旁通,广播,则发回管理服务端官博节点或者在上一层时就处理,此处代码不做优化
%% 私聊 private
gen_tcp:send(ClientSocket, term_to_binary({ok, "私聊频道:" ++ Msg})),
%% 世界 broadcast
chat_server:send({broadcast, {ok, "世界频道:" ++ Msg}}),
ok;
Error ->
?INFO("消息格式错误:~w", [Error])
end,
{noreply, State};
handle_info(Info, State) ->
?INFO("chat_client_handler handle_info:Info=~w,State=~w", [Info, State]),
{noreply, State}.
terminate(_Reason, #state{socket = Socket}) ->
gen_tcp:close(Socket),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
pid() ->
erlang:whereis(?MODULE).
chat_client.erl 连接服务端测试客户端
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @doc
%%% tcp 客户端简单搭建,其简单,学会搭建服务端即可搭建客户端了
%%% @end
%%% Created : 2023/8/15 18:54
%%%-------------------------------------------------------------------
-module(chat_client).
%% API
-export([start/0]).
-define(SERVER_IP, "127.0.0.1").
-define(SERVER_PORT, 8080).
start() ->
{ok, Socket} = gen_tcp:connect(?SERVER_IP, ?SERVER_PORT, [binary, {packet, 0}]),
error_logger:info_msg("客户端连接服务端成功"),
spawn(fun() -> loop_input(Socket) end),
receive_response(Socket).
loop_input(Socket) ->
MsgData = {ok, string:trim(io:get_line("请输入你的消息: "))},
gen_tcp:send(Socket, term_to_binary(MsgData)),
loop_input(Socket).
receive_response(Socket) ->
receive
{tcp, Socket, Bin} ->
case binary_to_term(Bin) of
{ok, Msg} ->
io:format("(收到聊天消息):~ts~n", [Msg]);
Error1 ->
error_logger:info_msg("Error receiving response: ~w~n", [Error1])
end;
Error2 ->
error_logger:info_msg("Error receiving response: ~w~n", [Error2])
end,
receive_response(Socket).
3. websocket 聊天系统(重要掌握点,类似以上的otp 聊天系统)
chat.app
{application, chat,
[{description, "rfos - chat server!"},
{id, "chat"},
{vsn, "0.1"},
{modules, [chat]},
{registered, [chat, chat_server_sup]},
{applications, [kernel, stdlib, sasl]},
{mod, {chat, []}},
{env, []}
]}.
chat.hrl
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2024,
%%% @doc
%%%
%%% @end
%%% Created : 02. 四月 2024 15:01
%%%-------------------------------------------------------------------
%%日志封装
-define(INFO(Format), error_logger:info_msg(Format, [])).
-define(INFO(Format, Args), error_logger:info_msg(Format, Args)).
-define(SERVER_IP, "127.0.0.1").
-define(SERVER_PORT, 8080).
-record(state, {socket, is_web_socket = false}).
-define(chat_client_list, chat_client_list). % 进程字典名称,用于存储各个聊天接口的连接,广播消息的需要[{ConnectSocket}]
-define(ROLE_PROCESS_EXIT_WAIT, 10).%%角色进程退出时等待多少秒
common_util.erl
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2010, rfos
%%% @doc
%%% 普通类 控制
%%% @end
%%%-------------------------------------------------------------------
-module(common_util).
-include("chat.hrl").
-export([manage_applications/6, start_applications/1, stop_applications/1, copy_files/3]).
manage_applications(Iterate, Do, Undo, SkipError, ErrorTag, Apps) ->
Iterate(fun(App, Acc) ->
case Do(App) of
ok ->
?INFO("manage_applications:~ts~ts", [App, "操作成功!"]),
[App | Acc];
{error, {SkipError, _}} -> Acc;
{error, Reason} ->
lists:foreach(Undo, Acc),
throw({error, {ErrorTag, App, Reason}})
end
end, [], Apps),
ok.
start_applications(Apps) ->
manage_applications(fun lists:foldl/3, fun application:start/1, fun application:stop/1, already_started, cannot_start_application, Apps).
stop_applications(Apps) ->
manage_applications(fun lists:foldr/3, fun application:stop/1, fun application:start/1, not_started, cannot_stop_application, Apps).
%% 文件复制,启动App时拷贝需要的文件
copy_files(SourceDir, DestDir, Extension) ->
FileList = filelib:wildcard(filename:join(SourceDir, "*" ++ Extension)),
io:format("TaegetFile:~p , FileList:~p ~n", [filename:join(SourceDir, "*" ++ Extension), FileList]),
lists:foreach(fun(File) ->
DestFile = filename:join(DestDir, filename:basename(File)),
file:copy(File, DestFile)
end, FileList).
chat_server_sup.erl
-module(chat_server_sup).
-behaviour(supervisor).
-include("chat.hrl").
-export([start_link/0]).
-export([init/1]).
-define(SERVER, ?MODULE).
start_link() ->
supervisor:start_link({local, ?SERVER}, ?MODULE, []).
init([]) ->
{ok, {{one_for_one, 10, 10}, []}}.
common_supervisor.erl
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2023,
%%% @doc
%%% 创建子监督树
%%% @end
%%%-------------------------------------------------------------------
-module(common_supervisor).
-export([start_child/2, start_child/3, start_supervisor/2]).
start_child(SupName, Module) ->
start_child(SupName, Module, 10 * 1000).
start_child(SupName, Module, ShutdownTimeout) ->
supervisor:start_child(SupName, {Module, {Module, start_link, []}, permanent, ShutdownTimeout, worker, [Module]}).
start_supervisor(SupName, Module) ->
supervisor:start_child(SupName, {Module, {Module, start_link, []}, permanent, 10 * 1000, supervisor, [Module]}).
chat_server.erl
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2024,
%%% @doc
%%% 聊天进程管理
%%% @end
%%% Created : 05. 四月 2024 13:17
%%%-------------------------------------------------------------------
-module(chat_server).
-behavior(gen_server).
-include("chat.hrl").
%% API
-export([init/1, handle_call/3, handle_cast/2, start/0, start_link/0, terminate/2, code_change/3, handle_info/2, pid/0, send/1]).
-export([get_data/0, set_data/1]).
start() ->
{ok, _} = common_supervisor:start_child(chat_server_sup, ?MODULE).
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
init([]) ->
erlang:process_flag(trap_exit, true),
{ok, ListenSocket} = do_async_listen(?SERVER_PORT),
?INFO("otp tcp 服务端 启动监听成功 ListenSocket=~w", [ListenSocket]),
{ok, #state{socket = ListenSocket}}.
send(Info) ->
pid() ! Info.
handle_call(_Request, _From, _State) ->
?INFO("handle_call:_Request=~w,_From=~w,_State=~w", [_Request, _From, _State]),
erlang:error(not_implemented).
handle_cast(_Request, _State) ->
?INFO("handle_cast:_Request=~w,State=~w", [_Request, _State]),
erlang:error(not_implemented).
%% 处理连接监听端口
handle_info({inet_async, ListenSocket, Ref, {ok, ClientSocket}}, State) ->
{ok, {ClientIp, ClientPort}} = inet:peername(ClientSocket),
{ok, Mod} = inet_db:lookup_socket(ListenSocket),
inet_db:register_socket(ClientSocket, Mod),
?INFO("accept_connect:{ClientSocket, Ref,ClientPort,ClientIp,Mod}=~w~n", [{ClientSocket, Ref, ClientPort, ClientIp, Mod}]),
prim_inet:async_accept(ListenSocket, -1),
%% 开启客户端连接聊天子进程处理
{ok, PID} = supervisor:start_child(chat_client_sup, [{ClientSocket}]),
%% 将socket监听移到该进程
gen_tcp:controlling_process(ClientSocket, PID),
{noreply, State};
%% 存储数据
handle_info({set_data, SocketState}, State) ->
set_data(SocketState),
{noreply, State};
%% 广播玩家
handle_info({broadcast, MsgData}, State) ->
ToList = get_data(),
?INFO("broadcast:MsgData=~w,List=~w", [MsgData, ToList]),
lists:foreach(fun(#state{socket = ClientSocket, is_web_socket = IsWebSocket}) ->
case IsWebSocket of
true ->
%% TODO websocket的数据需要特殊处理
ok;
false ->
gen_tcp:send(ClientSocket, term_to_binary(MsgData)),
ok
end
end, get_data()),
{noreply, State};
handle_info(Info, State) ->
?INFO("handle_info:Info=~w,State=~w", [Info, State]),
{noreply, State}.
terminate(_Reason, #state{socket = Socket} = State) ->
?INFO("terminate:_Reason=~w,State=~w", [_Reason, State]),
gen_tcp:close(Socket),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
pid() ->
erlang:whereis(?MODULE).
%% 进程字典操作
get_data() ->
case get(?chat_client_list) of
List when is_list(List) -> List;
_ -> []
end.
set_data(SocketState) ->
case get(?chat_client_list) of
List when is_list(List) ->
put(?chat_client_list, lists:keystore(SocketState#state.socket, #state.socket, List, SocketState));
_ ->
put(?chat_client_list, [SocketState])
end.
do_async_listen(Port) ->
Addr = {0, 0, 0, 0},
Opts = [binary, {reuseaddr, true}, {packet, 0}, {exit_on_close, false}, {active, false}, {send_timeout, 5000}, {keepalive, true}, {send_timeout_close, true}],
do_async_listen(Addr, Port, Opts).
do_async_listen(Addr, Port, Opts) ->
{ok, ListenSocket} = prim_inet:open(tcp, inet, stream),
inet_db:register_socket(ListenSocket, inet_tcp),
prim_inet:setopts(ListenSocket, Opts),
prim_inet:bind(ListenSocket, Addr, Port),
%% 队列的大小256
prim_inet:listen(ListenSocket, 256),
%% 监听并发的数量100
lists:foreach(fun(_) ->
prim_inet:async_accept(ListenSocket, -1)
end, lists:seq(1, 100)),
{ok, ListenSocket}.
chat_client_sup.erl
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2024,
%%% @doc
%%% 聊天进程管理-处理玩家消息进程的监督树
%%% @end
%%% Created : 05. 四月 2024 13:17
%%%-------------------------------------------------------------------
-module(chat_client_sup).
-include("chat.hrl").
-behavior(supervisor).
-export([start/0, start_link/0, init/1]).
start() ->
{ok, _Pid} = common_supervisor:start_supervisor(chat_server_sup, ?MODULE).
start_link() ->
supervisor:start_link({local, ?MODULE}, ?MODULE, []).
init([]) ->
ChildSpec = {
chat_client_handler,
{chat_client_handler, start_link, []},
temporary,
?ROLE_PROCESS_EXIT_WAIT,
worker,
[chat_client_handler]
},
{ok, {{simple_one_for_one, 1, 1}, [ChildSpec]}}.
chat_client_handler.erl
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2024,
%%% @doc
%%% 聊天进程管理-处理客户端消息进程
%%% @end
%%% Created : 05. 四月 2024 13:17
%%%-------------------------------------------------------------------
-module(chat_client_handler).
-behavior(gen_server).
-include("chat.hrl").
-export([start_link/1, init/1, pid/0, send/1]).
-export([handle_info/2, handle_cast/2, handle_call/3, terminate/2, code_change/3]).
start_link({NameArg}) ->
ServerName =
case NameArg of
ClientSocket1 when is_port(ClientSocket1) ->
{local, list_to_atom(port_to_list(ClientSocket1))};
{web_socket, ClientSocket2} when is_port(ClientSocket2) ->
{local, list_to_atom("web_socket:" ++ port_to_list(ClientSocket2))};
_ ->
{local, NameArg}
end,
?INFO("NameArg=~p", [NameArg]),
gen_server:start_link(ServerName, ?MODULE, [{NameArg}], []).
init([{{web_socket, ClientSocket}}]) ->
erlang:process_flag(trap_exit, true),
inet:setopts(ClientSocket, [{active, once}, {packet, 0}, binary]),
{ok, {IP, Port}} = inet:peername(ClientSocket),
?INFO("chat_client_handler web_socket {ClientSocket, IP, Port}=~w", [{ClientSocket, IP, Port}]),
SocketState = #state{socket = ClientSocket, is_web_socket = true},
chat_server:send({set_data, SocketState}),
{ok, SocketState};
init([{ClientSocket}]) ->
erlang:process_flag(trap_exit, true),
inet:setopts(ClientSocket, [{active, once}, {packet, 0}, binary]),
{ok, {IP, Port}} = inet:peername(ClientSocket),
?INFO("chat_client_handler {ClientSocket, IP, Port}=~w", [{ClientSocket, IP, Port}]),
SocketState = #state{socket = ClientSocket, is_web_socket = false},
chat_server:send({set_data, SocketState}),
{ok, #state{socket = ClientSocket}};
init([NameArg]) ->
erlang:process_flag(trap_exit, true),
?INFO("chat_client_handler NameArg=~w", [{NameArg}]),
{ok, #state{}}.
send(Info) ->
pid() ! Info.
handle_call(_Request, _From, _State) ->
erlang:error(not_implemented).
handle_cast(_Request, _State) ->
erlang:error(not_implemented).
%% _Socket = ClientSocket 判断处理socket连接和websocket连接,这里同时能接收websocket连接握手数据和socket连接数据,
handle_info({tcp, _Socket, Data}, #state{socket = ClientSocket, is_web_socket = false} = State) ->
inet:setopts(ClientSocket, [{active, once}, {packet, 0}, binary]),
case binary:match(Data, <<"Upgrade: websocket\r\n">>) of
nomatch -> %% 处理 socket msg
case binary_to_term(Data) of
{ok, Msg} ->
io:format("收到消息:socket:~w, msg:~ts~n", [ClientSocket, Msg]),
%% 如果私聊,则直接发送给玩家即可,触类旁通,广播,则发回管理服务端广播节点或者在上一层时就处理,此处代码不做优化
%% 私聊 private
gen_tcp:send(ClientSocket, term_to_binary({ok, "私聊频道:" ++ Msg})),
%% 世界 broadcast
chat_server:send({broadcast, {ok, "世界频道:" ++ Msg}}),
ok;
Error ->
?INFO("消息格式错误:~w", [Error])
end,
ok;
_ -> %% web_socket 处理握手请求
case websocket_handshake(Data) of
{ok, Headers, true} ->
?INFO("WebSocketData:~p", [{Headers}]),
WebSocketKey =
case lists:keyfind("Sec-WebSocket-Key", 1, Headers) of
{_, Key} -> Key;
_ -> ""
end,
%% 根据WebSocketKey计算Sec-WebSocket-Accept的值
Challenge = base64:encode(sha(list_to_binary(WebSocketKey ++ "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))),
Response = [
"HTTP/1.1 101 Switching Protocols\r\n",
"Connection: Upgrade\r\n",
"Upgrade: websocket\r\n",
"Sec-websocket-accept: ", Challenge, "\r\n",
"\r\n", <<>>
],
gen_tcp:send(ClientSocket, list_to_binary(Response)),
?INFO("websocket_handshake success"),
%% web_socket 接收的数据,重新放置一个进程处理,也可以同一个进程处理,但是数据解析与socket接收的到的数据不同
{ok, PID} = supervisor:start_child(chat_client_sup, [{{web_socket, ClientSocket}}]),
%% 将socket监听移到该进程
gen_tcp:controlling_process(ClientSocket, PID),
ok;
Error ->
?INFO("Error:~p", [{Error}])
end,
ok
end,
{noreply, State};
%% 处理 web_socket msg %% TODO websocket的数据需要特殊处理,解析数据帧,字符串转binary数据格式交互
handle_info({tcp, _Socket, Data}, #state{socket = ClientSocket, is_web_socket = true} = State) ->
inet:setopts(ClientSocket, [{active, once}, {packet, 0}, binary]),
?INFO("{tcp, _Socket, Data}=~p,State=~p", [{tcp, _Socket, Data}, State]),
%% 返回数据,socket连接后基于tcp协议数据交互
gen_tcp:send(ClientSocket, list_to_binary("{ok, success}")),
{noreply, State};
handle_info(Info, State) ->
?INFO("chat_client_handler handle_info:Info=~p,State=~p", [Info, State]),
{noreply, State}.
terminate(_Reason, #state{socket = Socket}) ->
gen_tcp:close(Socket),
ok.
code_change(_OldVsn, State, _Extra) ->
{ok, State}.
pid() ->
erlang:whereis(?MODULE).
%% 解析 WebSocket 握手请求,提取请求头部信息
websocket_handshake(Data) ->
AimData = binary_to_list(Data),
case catch string:split(AimData, "\r\n\r\n", trailing) of
[HeadersData, _] ->
case catch string:split(HeadersData, "\r\n", all) of
[_Head | HeadersList] ->
Headers =
lists:map(fun(E) ->
[Arg, Val] = string:split(E, ":"),
{string:trim(Arg), string:trim(Val)}
end, HeadersList),
IsWebSocket = true,
{ok, Headers, IsWebSocket};
_ ->
{error, [], false}
end;
_ ->
{error, [], false}
end.
sha(Bin) ->
case catch crypto:hash(sha, Bin) of
{'EXIT', _} -> crypto:sha(Bin);
Value -> Value
end.
websocket_client.erl websocket测试客户端,也可使用一些接口工具测试,还可使用以上原声带额chat_client.erl 进行socket连接
%%%-------------------------------------------------------------------
%%% @author rfos
%%% @copyright (C) 2024,
%%% @doc
%%% 模拟websocket连接客户端
%%% @end
%%% Created : 02. 四月 2024 15:01
%%%-------------------------------------------------------------------
-module(websocket_client).
-include("chat.hrl").
-export([start/2, start/0, receive_messages1/1, receive_messages2/1]).
start() ->
start(?SERVER_IP, ?SERVER_PORT).
start(ServerHost, ServerPort) ->
{ok, Socket} = gen_tcp:connect(ServerHost, ServerPort, [{active, once}, binary]),
handshake(Socket).
handshake(Socket) ->
%% 握手请求
HandshakeRequest = "GET / HTTP/1.1\r\n" ++
"Host: " ++ atom_to_list(node()) ++ "\r\n" ++
"Upgrade: websocket\r\n" ++
"Connection: Upgrade\r\n" ++
"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n" ++
"Sec-WebSocket-Version: 13\r\n\r\n",
gen_tcp:send(Socket, HandshakeRequest),
%% 接收到握手请求响应的数据
receive_handshake(Socket),
spawn(fun() -> loop_input(Socket) end),
receive_messages1(Socket).
loop_input(Socket) ->
MsgData = {ok, string:trim(io:get_line("请输入你的消息: "))},
gen_tcp:send(Socket, term_to_binary(MsgData)),
loop_input(Socket).
receive_handshake(Socket) ->
receive
{tcp, Socket, Data} ->
HandshakeResponse = binary_to_list(Data),
io:format("Received handshake response: ~s~n", [HandshakeResponse]);
{tcp_closed, Socket} ->
io:format("Server closed the connection during handshake~n", [])
end.
%% 方法1
receive_messages1(Socket) ->
case gen_tcp:recv(Socket, 0) of %% 阻塞式接收
{ok, SocketData} ->
%% 解析字符串二进制数据
Res = iolist_to_binary(SocketData),
case binary_to_list(Res) of
MsgData when is_list(MsgData) ->
io:format("(收到聊天消息):~ts~n", [MsgData]);
Error1 ->
?INFO("Error receiving response: ~w~n", [Error1])
end;
Error2 ->
?INFO("Error receiving response: ~w~n", [Error2])
end,
receive_messages1(Socket).
%% 方法 2
receive_messages2(Socket) ->
%% websocket连接与普通的socket连接不一样,需要加这个才能接收多次,有消息通知执行
inet:setopts(Socket, [{active, once}, {packet, 0}, binary]),
receive
{tcp, Socket, Bin} ->
%% 解析字符串二进制数据
case binary_to_list(Bin) of
MsgData when is_list(MsgData) ->
io:format("(收到聊天消息):~ts~n", [MsgData]);
Error1 ->
?INFO("Error receiving response: ~w~n", [Error1])
end;
Error2 ->
?INFO("Error receiving response: ~w~n", [Error2])
end,
receive_messages2(Socket).
%%在Erlang中,对于原生的Socket客户端,可以使用gen_tcp模块来创建Socket连接。当你使用gen_tcp:recv函数来接收数据时,该函数默认是阻塞式的,即它会一直等待直到有数据到来。因此,即使不设置{active, once}选项,也可以接收消息。
%%
%%但是,对于WebSocket客户端,情况稍有不同。WebSocket是一种基于TCP的协议,但它有自己的消息格式和协议规则。在Erlang中,要实现WebSocket客户端,你可以使用gun或websocket_client等库,这些库提供了对WebSocket协议的更高级别的支持。
%%
%%WebSocket是一种双向通信协议,它允许服务器和客户端之间进行实时的数据交换。通常情况下,WebSocket客户端需要不断地接收来自服务器的数据,而不是像普通的Socket一样只接收一次。因此,为了实现这一点,你需要设置{active, once}选项,以便在接收到新数据时,Erlang能够自动通知你的代码,并让你的代码继续执行,而不是阻塞等待数据的到达。
%%
%%综上所述,在使用WebSocket时,需要设置{active, once}选项来告诉Erlang在有新数据到达时主动通知你的代码。而对于普通的Socket客户端,即使不设置该选项,也可以接收到消息,因为gen_tcp:recv函数默认是阻塞式的,会一直等待直到有数据到来。
end 源码下载地址
https://pan.baidu.com/s/1W44H7dBXfVAPUxJz_3ccEg?pwd=rfos
提取码:rfos