Erlang 入门——从普通tcp到OTP框架通信

根据Erlang的语言特点,Erlang创建进程就如同Java创建对象那样简单。而Erlang的OTP框架,可以理解为是Java的Spring框架。

刚入门Erlang的tcp通信,书上的写法是根据socket用gen_tcp:send和receive通信,到了OTP里用gen_server也是一样的原理,只不过在OTP框架下gen_server行为模式封装了一些方法使得写法更方便。

首先是一般的写法。server端监听来自client的tcp连接。

%% server.erl
start(Port) ->
	{ok, LSocket} = gen_tcp:listen(Port, [binary, {packet, 4}, {active, true}, {reuseaddr, true}]),
	do_accept(LSocket).
	
do_accept(LSocket) ->
	{ok, Socket} = gen_tcp:accept(LSocket),
	io:format("Socket ~p connnected. ~n", [Socket]).
%% client.erl
start(Port) ->
	{ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {packet, 4}]).

连接后生成的socket用于后续的交互通信,服务端端通常写个loop函数,监听来自客户端请求。而这个loop函数应该给它新建一进程,使其不影响服务端的tcp监听,通常这么写。

%% server.erl
	Pid = spawn(fun() -> loop(Socket) end),
	gen_tcp:controlling_process(Socket, Pid).

gen_tcp:controlling_process的作用,我的理解是给loop函数创建一进程后,将这个进程号和Socket绑定,作用就是将tcp消息转换成进程消息,使其能通过{tcp, Socket, Bin}自动匹配接收。

整个demo如下:

-module(server).

-export([start/1]).

start(Port) ->
	{ok, LSocket} = gen_tcp:listen(Port, [binary, {packet, 4}, {active, true}, {reuseaddr, true}]),
	do_accept(LSocket).
	
do_accept(LSocket) ->
	{ok, Socket} = gen_tcp:accept(LSocket),
	io:format("Socket ~p connnected. ~n", [Socket]),
	Pid = spawn(fun() -> loop(Socket) end),
	gen_tcp:controlling_process(Socket, Pid),
	loop(Socket).
	
loop(Socket) ->
	receive
		{tcp, Socket, Bin} ->
			Str = binary_to_term(Bin),
			io:format("Server get the msg : ~p ~n", [Str]),
			gen_tcp:send(Socket, term_to_binary(hi)),
			loop(Socket);
		{tcp_closed, Socket} ->
			io:format("Socket ~p disconnected ~n", [Socket])
	end.	
-module(client).
-export([start/1, send/1]).

start(Port) ->
	ets:new(tcpname, [set, public, named_table]),
	{ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {packet, 4}]),
	ets:insert(tcpname, {socket, Socket}),
	Pid = spawn(fun() -> loop(Socket) end),
	gen_tcp:controlling_process(Socket, Pid).
	
send(Str) ->
	Socket = ets:lookup_element(tcpname, socket, 2),
	gen_tcp:send(Socket, term_to_binary(Str)).
	
loop(Socket) ->
	receive
		{tcp, Socket, Bin} ->
			Str = binary_to_term(Bin),
			io:format("Client get the msg : ~p ~n", [Str]),
			loop(Socket)
	end.

效果如下:
服务端:
Erlang 入门——从普通tcp到OTP框架通信_第1张图片
客户端:
Erlang 入门——从普通tcp到OTP框架通信_第2张图片

在OTP框架中,gen_server做了封装,新建进程不用spawn而是用gen_server:start_link,而接收来自tcp的消息用handle_info加字段匹配进行接收即可。

根据gen_server:start_link的写法,第二个参数为Module,即在gen_server里新建的进程将是一个新的模块(新的erl文件)。

%% server.erl
	%%Pid = spawn(fun() -> handle_clients(Socket) end),
	{ok, PidA} = gen_server:start_link(server_recv, Socket, []),

这么做的结果就是,server_recv将会接收来自client的tcp消息,并且不用receive,直接用handle_info即可接收,直接用字段匹配接收tcp消息将使得开发效率得到很大的提升。

%% server_recv.erl
handle_info({tcp, Socket, Bin}, State) ->
	Msg = binary_to_term(Bin),
	io:format("~p ~n", [Msg]),
	{noreply, State};

而在gen_server里,State用来保存服务器的状态,那么你可以定义一个record,将tcp连接生成的Socket保存到State里,那么在整个服务器所有地方(handle_call、handle_cast、handle_info)里都能取出来用。

(客户端将Socket保存到状态后,在需要发送消息时可从状态里取出来用)

%% client.erl
-record(state, {socket}).

...

init(Port) ->
  {ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {packet, 4}]),
  {ok, #state{socket = LSocket}}.

...

handle_cast({login, Data}, State) ->
  Socket = State#state.socket,
  gen_tcp:send(Socket, term_to_binary(Data)),
  {noreply, State};

完整demo如下:
① 服务端server:

-module(server).
-behaviour(gen_server).
-export([start/1]).

-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(TCP_OPTIONS, [binary, {packet, 4}, {active, true}, {reuseaddr, true}]).

-record(state, {socket}).

start(Port) ->
	gen_server:start_link({local, ?SERVER}, ?MODULE, Port, []).
	
init(Port) ->
	{ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
	self() ! {to_accept, LSocket},
	{ok, #state{socket = LSocket}}.
	
do_accept(LSocket) ->
	{ok, Socket} = gen_tcp:accept(LSocket),
	io:fwrite("Socket connected: ~w ~n", [Socket]),
	%%Pid = spawn(fun() -> handle_clients(Socket) end),
	{ok, Pid} = gen_server:start_link(server_recv, Socket, []),
	gen_tcp:controlling_process(Socket, Pid),
	do_accept(LSocket).
	
handle_info({to_accept, LSocket}, State) ->
	do_accept(LSocket),
	{noreply, State}.
	
handle_call(stop, _From, Tab) ->
	{stop, normal, stopped, Tab}.
	
handle_cast(_Msg, State) ->	
	{noreply, State}.
	
terminate(_Reason, _State) ->
	ok.
code_change(_OldVsn, State, _Extra) ->
	{ok, State}.

② 接收客户端消息的新进程server_recv:

-module(server_recv).
-behaviour(gen_server).

-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).
-define(TCP_OPTIONS, [binary, {packet, 4}, {active, true}, {reuseaddr, true}]).

-record(state, {socket}).

init(Socket) ->
	{ok, #state{socket = Socket}}.

handle_info({hi, Data}, State) ->
	Socket = State#state.socket,
	gen_tcp:send(Socket, term_to_binary(Data)),
	{noreply, State};

handle_info({tcp, Socket, Bin}, State) ->
	Msg = binary_to_term(Bin),
	io:format("server get the msg : ~p ~n", [Msg]),
	Msg1 = hi,
	self() ! {hi, Msg1},	
	{noreply, State};
	
handle_info({tcp_closed, Socket}, State) ->
	io:fwrite("Socket disconnected: ~w ~n", [Socket]),
	{noreply, State}.
	
handle_call(stop, _From, Tab) ->
	{stop, normal, stopped, Tab}.

handle_cast(_Msg, State) ->
	{noreply, State}.

terminate(_Reason, _State) ->
	ok.
code_change(_OldVsn, State, _Extra) ->
	{ok, State}.

③ 客户端client:

-module(client).
-behaviour(gen_server).
-export([start/1, hello/0]).

-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
-define(SERVER, ?MODULE).

-record(state, {socket}).

start(Port) ->
	gen_server:start_link({local, ?SERVER}, ?MODULE, Port, []).
	
init(Port) ->
	{ok, Socket} = gen_tcp:connect("localhost", Port, [binary, {packet, 4}]),
	{ok, #state{socket = Socket}}.
	
hello() ->
	Data = hello,
	gen_server:cast(?MODULE, {hello, Data}).

handle_info({tcp, Socket, Bin}, State) ->
	Msg = binary_to_term(Bin),
	io:format("client get the msg : ~p ~n", [Msg]),
	{noreply, State};
	
handle_info({tcp_closed, Socket}, State) ->
	io:fwrite("Socket disconnected: ~w ~n", [Socket]),
	{noreply, State}.

handle_cast({hello, Data}, State) ->
	Socket = State#state.socket,
	gen_tcp:send(Socket, term_to_binary(Data)),
	{noreply, State}.

handle_call(stop, _From, State) ->
	{stop, normal, stopped, State}.

terminate(_Reason, _State) ->
	ok.
code_change(_OldVsn, State, Extra) ->
	{ok, State}.

小白入门分享,如有错误,欢迎指正。如有帮助,欢迎点赞收藏~

你可能感兴趣的:(Erlang,tcp/ip,erlang,网络,demo,OTP)