Erlang聊天室

Erlang 聊天室

因为之前有使用过PyQt做过聊天室的功能,现在使用Erlang去重写聊天室,去了解这门语言,有兴趣的朋友可以写基于Qt客户端和Erlang的服务端去交互,本Demo统一使用Erlang去实现客户端。

所需知识

  • 网络编程中接受请求的套接字创建过程如下(四步记忆)
    • 第一步:调用socket函数
    • 第二步:调用bind函数分配IP地址和端口
    • 第三步:调用listen函数转为可接收请求状态
    • 第四步:调用accept函数受理连接请求

服务端监听Socket

  • 第一部分初始化ets表,可选参数可自由选择
start_server() ->
	ets:new(id, [ordered_set, public, named_table, {write_concurrency, true}, {read_concurrency, true}]),
	case gen_tcp:listen(1234, [binary, {packet, 0}, {active, true}]) of
		{ok, ListenSocket} ->
			spawn(fun() -> client_connect(ListenSocket) end);
		{error, Reason} ->
			io:format("~p~n", [Reason])
	end.
  • 第二部分处理请求
    • 这部分要注意gen_tcp:accept()函数,这部分会处理本次请求才会去处理新的请求,即顺序处理,为了解决这个问题,使用了并发处理,每次连接都会开一个新进程去处理,这样就可以并发处理了。
client_connect(ListenSocket) ->
	case gen_tcp:accept(ListenSocket) of
		{ok, Socket} ->
			%% 进行验证,看是否是注册还是登录
			spawn(fun() -> client_connect(ListenSocket) end),
			loop(Socket);
		{error, Reason} ->
			io:format("~p~n", [Reason])
	end.
  • 第三部分While True循环
    • 这部分分别去处理客户端的发送信息,如注册,登录,退出,私聊,群聊。
loop(Socket) ->
	receive
		{tcp, Socket, Bin} ->
			[Id, Sign, PassWord, SendId, MessageInfos] = binary_to_term(Bin),
			if
				Sign =:= register_user ->
					Info = register_user(Id, PassWord, Socket),
					gen_tcp:send(Socket, term_to_binary(Info)),
					loop(Socket);
				Sign =:= login_user ->
					Info = login_user(Id, PassWord, Socket),
					gen_tcp:send(Socket, term_to_binary(Info)),
					loop(Socket);
				Sign =:= login_out ->
					Info = login_out(Id, Socket),
					gen_tcp:send(Socket, term_to_binary(Info)),
					loop(Socket);
				Sign =:= private_msg ->
					private_chat(SendId, Socket, MessageInfos),
					loop(Socket);
				Sign =:= group_msg ->
					group_chat(Socket, MessageInfos),
					loop(Socket);
				true ->
					io:format("error sign ~n"),
					loop(Socket)
			end;
		{tcp_closed, Socket} ->
			io:format("Server socket closed ~n")
	end.
  • 功能部分

    • 用户注册
    • 使用ets库的lookup函数去检查用户是否已经注册
    %% 用户注册
    register_user(Id, PassWord, Socket) ->
    	case ets:lookup(id, Id) of
    		[_Ok] ->
    			io:format("Account is fail ~n"),
    			"Account is exist ~n";
    		_ ->
    			ets:insert(id, {Id, PassWord, 0, Socket}),
    			"register successed ~n"
    	end.
    
    • 用户登录
    • 首先使用ets库的match_object函数的去检查用户登录情况,这里其实写详细点,就是有三种可能,用户账号不存在,密码错误,用户已经在线。同时如果登录成功,应该设置用户为在线状态和给用户分配socket。
    %% 用户登录
    login_user(Id, PassWord, Socket) ->
    	case ets:match_object(id, {Id, PassWord, 0, Socket}) of
    		[_Ok] ->
    			ets:update_element(id, Id, [{3, 1}, {4, Socket}]),
    			"login successed";
    		Reson ->
    			io:format("login is fail ~n ~p", [Reson]),
    			"Password error or Account is not exist ~n"
    	end.
    
    • 用户退出
    • 退出用户,首先判断用户已经在线,才能下线,同时把在线状态和socket设置为0。
    %% 退出用户
    login_out(Id, Socket) ->
    	%% 因为id对应唯一socket,所以不需要PassWord
    	case ets:match_object(id, {Id, '_', 1, Socket}) of
    		[_Ok] ->
    			ets:update_element(id, Id, [{3, 0}, {4, 0}]),
    			"login successed";
    		_ ->
    			io:format("out is fail ~n"),
    			"login is fail"
    	end.
    
    • 群聊
    • 首先判断发送用户是否在线,然后递归给在线用户发送信息。
    %% 群聊
    group_chat(Socket, MessageInfos) ->
    	case ets:match_object(id, {'_', '_', 1, Socket}) of
    		[{Id, _, _, _}] ->
    			Res = ets:match_object(id, {'_', '_', 1, '_'}),
    			case Res =:= [] of
    				true ->
    					io:format("no person online ~p ~n", [Res]);
    				_ ->
    					group_send_msg(Res, Id, MessageInfos)
    			end;
    		_ ->
    			io:format("group chat is fail ~n")
    	end.
    
    
    %% 群聊发送
    group_send_msg([], _Id, _MessageInfos) ->
    	next;
    group_send_msg([Info | Infos], Id, MessageInfos) ->
    	{_, _, _, Socket} = Info,
    	gen_tcp:send(Socket, term_to_binary("from: " ++ integer_to_list(Id) ++ "say: " ++ MessageInfos)),
    	group_send_msg(Infos, Id, MessageInfos).
    
    • 在线私聊
    • 由于没有设置字段去存取聊天记录,所以使用只有在线状态下的聊天。
    %% 在线私聊
    private_chat(SendId, Socket, MessageInfos) ->
    	case ets:match_object(id, {'_', '_', 1, Socket}) of
    		[{Id, _, _, _}] ->
    			Res = ets:match_object(id, {SendId, '_', 1, '_'}),
    			case Res =:= [] of
    				true ->
    					io:format("send person not online ~p ~n", [Res]);
    				_ ->
    					private_send_msg(Res, Id, MessageInfos)
    			end;
    		_ ->
    			io:format("private chat is fail ~n")
    	end.
    
    %% 私聊发送
    private_send_msg([Info], Id, MessageInfos) ->
    	{_, _, _, Socket} = Info,
    	gen_tcp:send(Socket, term_to_binary("from: " ++ integer_to_list(Id) ++ "say: " ++ MessageInfos)).
    

客户端实现

  • 客户端进程
  • 并把socket控制进程设置为loop的进程号
%客户端
start_client() ->
	{ok, Socket} = gen_tcp:connect("localhost", 1234, [binary, {packet, 0}]),  %连接服务器
	%新建一个进程负责接收消息
	Pid = spawn(fun() -> loop() end),
	gen_tcp:controlling_process(Socket, Pid),
	sendMsg(Socket).

loop() ->
	receive
		{tcp, _Socket, Bin} ->
			Res = binary_to_term(Bin),
			io:format("Message Info! ~p ~n", [Res]),
			loop();
		{tcp_closed, _Socket} ->
			io:format("Socket is closed! ~p ~n")
	end.
  • 客户端操作
  • 对应服务端的功能部分
sendMsg(Socket) ->
	S = io:get_line("select operation: "),
	{Sign, _Info} = string:to_integer(S),
	SendMsg = operation_message(Sign),
	gen_tcp:send(Socket, term_to_binary(SendMsg)),
	sendMsg(Socket).

%% 用户注册
operation_message(1) ->
	I = io:get_line("id: "),
	{Id, _Info} = string:to_integer(I),
	Password = io:get_line("register password: "),
	[Id, register_user, Password, 0, 0];
%% 用户登录
operation_message(2) ->
	I = io:get_line("id:"),
	Password = io:get_line("login password: "),
	{Id, _Info} = string:to_integer(I),
	[Id, login_user, Password, 0, 0];
%% 用户退出
operation_message(3) ->
	I = io:get_line("id: "),
	{Id, _Info} = string:to_integer(I),
	[Id, login_out, 0, 0, 0];
%% 私聊
operation_message(4) ->
	Sd = io:get_line("send_id: "),
	Msg = io:get_line("MsgInfo: "),
	{SendId, _Info} = string:to_integer(Sd),
	[0, private_msg, 0, SendId, Msg];
%% 群聊
operation_message(5) ->
	Msg = io:get_line("MsgInfo: "),
	[0, group_msg, 0, 0, Msg];
%% 无效操作
operation_message(_) ->
	Msg = io:format("invalid_operation ~n"),
	[0, invalid_operation, 0, 0, Msg].

完整源代码

  • 服务端
%%%-------------------------------------------------------------------
%%% @author Curry
%%% @copyright (C) 2021, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 08. 8月 2021 13:33
%%%-------------------------------------------------------------------
-module(chatserv).
-author("Curry").

%% API
-compile(export_all).
-import(ets, [insert_new/2]).

start_server() ->
	ets:new(id, [ordered_set, public, named_table, {write_concurrency, true}, {read_concurrency, true}]),
	case gen_tcp:listen(1234, [binary, {packet, 0}, {active, true}]) of
		{ok, ListenSocket} ->
			spawn(fun() -> client_connect(ListenSocket) end);
		{error, Reason} ->
			io:format("~p~n", [Reason])
	end.


client_connect(ListenSocket) ->
	case gen_tcp:accept(ListenSocket) of
		{ok, Socket} ->
			%% 进行验证,看是否是注册还是登录
			spawn(fun() -> client_connect(ListenSocket) end),
			loop(Socket);
		{error, Reason} ->
			io:format("~p~n", [Reason])
	end.

loop(Socket) ->
	receive
		{tcp, Socket, Bin} ->
			[Id, Sign, PassWord, SendId, MessageInfos] = binary_to_term(Bin),
			if
				Sign =:= register_user ->
					Info = register_user(Id, PassWord, Socket),
					gen_tcp:send(Socket, term_to_binary(Info)),
					loop(Socket);
				Sign =:= login_user ->
					Info = login_user(Id, PassWord, Socket),
					gen_tcp:send(Socket, term_to_binary(Info)),
					loop(Socket);
				Sign =:= login_out ->
					Info = login_out(Id, Socket),
					gen_tcp:send(Socket, term_to_binary(Info)),
					loop(Socket);
				Sign =:= private_msg ->
					private_chat(SendId, Socket, MessageInfos),
					loop(Socket);
				Sign =:= group_msg ->
					group_chat(Socket, MessageInfos),
					loop(Socket);
				true ->
					io:format("error sign ~n"),
					loop(Socket)
			end;
		{tcp_closed, Socket} ->
			io:format("Server socket closed ~n")
	end.

%% 用户注册
register_user(Id, PassWord, Socket) ->
	case ets:lookup(id, Id) of
		[_Ok] ->
			io:format("Account is fail ~n"),
			"Account is exist ~n";
		_ ->
			ets:insert(id, {Id, PassWord, 0, Socket}),
			"register successed ~n"
	end.

%% 用户登录
login_user(Id, PassWord, Socket) ->
	case ets:match_object(id, {Id, PassWord, 0, Socket}) of
		[_Ok] ->
			ets:update_element(id, Id, [{3, 1}, {4, Socket}]),
			"login successed";
		Reson ->
			io:format("login is fail ~n ~p", [Reson]),
			"Password error or Account is not exist ~n"
	end.

%% 退出用户
login_out(Id, Socket) ->
	%% 因为id对应唯一socket,所以不需要PassWord
	case ets:match_object(id, {Id, '_', 1, Socket}) of
		[_Ok] ->
			ets:update_element(id, Id, [{3, 0}, {4, 0}]),
			"login successed";
		_ ->
			io:format("out is fail ~n"),
			"login is fail"
	end.

%% 群聊
group_chat(Socket, MessageInfos) ->
	case ets:match_object(id, {'_', '_', 1, Socket}) of
		[{Id, _, _, _}] ->
			Res = ets:match_object(id, {'_', '_', 1, '_'}),
			case Res =:= [] of
				true ->
					io:format("no person online ~p ~n", [Res]);
				_ ->
					group_send_msg(Res, Id, MessageInfos)
			end;
		_ ->
			io:format("group chat is fail ~n")
	end.


%% 群聊发送
group_send_msg([], _Id, _MessageInfos) ->
	next;
group_send_msg([Info | Infos], Id, MessageInfos) ->
	{_, _, _, Socket} = Info,
	gen_tcp:send(Socket, term_to_binary("from: " ++ integer_to_list(Id) ++ "say: " ++ MessageInfos)),
	group_send_msg(Infos, Id, MessageInfos).

%% 在线私聊
private_chat(SendId, Socket, MessageInfos) ->
	case ets:match_object(id, {'_', '_', 1, Socket}) of
		[{Id, _, _, _}] ->
			Res = ets:match_object(id, {SendId, '_', 1, '_'}),
			case Res =:= [] of
				true ->
					io:format("send person not online ~p ~n", [Res]);
				_ ->
					private_send_msg(Res, Id, MessageInfos)
			end;
		_ ->
			io:format("private chat is fail ~n")
	end.

%% 私聊发送
private_send_msg([Info], Id, MessageInfos) ->
	{_, _, _, Socket} = Info,
	gen_tcp:send(Socket, term_to_binary("from: " ++ integer_to_list(Id) ++ "say: " ++ MessageInfos)).
  • 客户端
%%%-------------------------------------------------------------------
%%% @author Curry
%%% @copyright (C) 2021, <COMPANY>
%%% @doc
%%%
%%% @end
%%% Created : 08. 8月 2021 14:03
%%%-------------------------------------------------------------------
-module(chatclient).
-author("Curry").

%% API
-compile(export_all).

%客户端
start_client() ->
	{ok, Socket} = gen_tcp:connect("localhost", 1234, [binary, {packet, 0}]),  %连接服务器
	%新建一个进程负责接收消息
	Pid = spawn(fun() -> loop() end),
	gen_tcp:controlling_process(Socket, Pid),
	sendMsg(Socket).

loop() ->
	receive
		{tcp, _Socket, Bin} ->
			Res = binary_to_term(Bin),
			io:format("Message Info! ~p ~n", [Res]),
			loop();
		{tcp_closed, _Socket} ->
			io:format("Socket is closed! ~p ~n")
	end.

sendMsg(Socket) ->
	S = io:get_line("select operation: "),
	{Sign, _Info} = string:to_integer(S),
	SendMsg = operation_message(Sign),
	gen_tcp:send(Socket, term_to_binary(SendMsg)),
	sendMsg(Socket).

%% 用户注册
operation_message(1) ->
	I = io:get_line("id: "),
	{Id, _Info} = string:to_integer(I),
	Password = io:get_line("register password: "),
	[Id, register_user, Password, 0, 0];
%% 用户登录
operation_message(2) ->
	I = io:get_line("id:"),
	Password = io:get_line("login password: "),
	{Id, _Info} = string:to_integer(I),
	[Id, login_user, Password, 0, 0];
%% 用户退出
operation_message(3) ->
	I = io:get_line("id: "),
	{Id, _Info} = string:to_integer(I),
	[Id, login_out, 0, 0, 0];
%% 私聊
operation_message(4) ->
	Sd = io:get_line("send_id: "),
	Msg = io:get_line("MsgInfo: "),
	{SendId, _Info} = string:to_integer(Sd),
	[0, private_msg, 0, SendId, Msg];
%% 群聊
operation_message(5) ->
	Msg = io:get_line("MsgInfo: "),
	[0, group_msg, 0, 0, Msg];
%% 无效操作
operation_message(_) ->
	Msg = io:format("invalid_operation ~n"),
	[0, invalid_operation, 0, 0, Msg].
原文地址:https://www.cnblogs.com/572354941hnit/p/15115880.html