根据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.
在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}.
小白入门分享,如有错误,欢迎指正。如有帮助,欢迎点赞收藏~