基于Erlang开发入门聊天小demo(普通socket(tcp&udp)聊天、otp整合tcp以及websocket聊天、使用Emakefile编译所有项目)及附源码地址、学会入门Erlang

这篇博客介绍了如何使用Erlang开发聊天应用,包括原生socket实现的TCP和UDP聊天,以及基于OTP框架的TCP聊天服务端和WebSocket聊天系统。还详细讲解了项目结构、编译过程以及运行方法,并提供了源码下载链接。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


注意: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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

锐行织梦者

谢谢您的支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值