前面几篇文章里谈到了Erlang的
gen_tcp网络编程和Erlang/OPT的
gen_server模块,现在让我们将它们两者绑定在一起
大多数人认为“服务器”意味着网络服务器,但Erlang使用这个术语时表达的是更抽象的意义
gen_serer在Erlang里是基于它的消息传递协议来操作的服务器,我们可以在此基础上嫁接一个TCP服务器,但这需要一些工作
网络服务器的结构
大部分网络服务器有相似的架构
首先它们创建一个监听socket来监听接收的连接
然后它们进入一个接收状态,在这里一直循环接收新的连接,直到结束(结束表示连接已经到达并开始真正的client/server工作)
先看看前面网络编程里的echo server的例子:
- -module(echo).
- -author('Jesse E.I. Farmer <[email protected]>').
- -export([listen/1]).
-
- -define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]).
-
- % Call echo:listen(Port) to start the service.
- listen(Port) ->
- {ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
- accept(LSocket).
-
- % Wait for incoming connections and spawn the echo loop when we get one.
- accept(LSocket) ->
- {ok, Socket} = gen_tcp:accept(LSocket),
- spawn(fun() -> loop(Socket) end),
- accept(LSocket).
-
- % Echo back whatever data we receive on Socket.
- loop(Socket) ->
- case gen_tcp:recv(Socket, 0) of
- {ok, Data} ->
- gen_tcp:send(Socket, Data),
- loop(Socket);
- {error, closed} ->
- ok
- end.
-module(echo).
-author('Jesse E.I. Farmer <[email protected]>').
-export([listen/1]).
-define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]).
% Call echo:listen(Port) to start the service.
listen(Port) ->
{ok, LSocket} = gen_tcp:listen(Port, ?TCP_OPTIONS),
accept(LSocket).
% Wait for incoming connections and spawn the echo loop when we get one.
accept(LSocket) ->
{ok, Socket} = gen_tcp:accept(LSocket),
spawn(fun() -> loop(Socket) end),
accept(LSocket).
% Echo back whatever data we receive on Socket.
loop(Socket) ->
case gen_tcp:recv(Socket, 0) of
{ok, Data} ->
gen_tcp:send(Socket, Data),
loop(Socket);
{error, closed} ->
ok
end.
你可以看到,listen会创建一个监听socket并马上调用accept
accept会等待进来的连接,创建一个新的worker(loop)来处理真正的工作,然后等待下一个连接
在这部分代码里,父进程拥有listen socket和accept loop两者
后面我们会看到,如果我们集成accept/listen loop和gen_server的话这样做并不好
抽象网络服务器
网络服务器有两部分:连接处理和业务逻辑
上面讲到,连接处理对每个网络服务器都是几乎一样的
理想状态下我们可以这样做:
- -module(my_server).
- start(Port) ->
- connection_handler:start(my_server, Port, businees_logic).
-
- business_logic(Socket) ->
- % Read data from the network socket and do our thang!
-module(my_server).
start(Port) ->
connection_handler:start(my_server, Port, businees_logic).
business_logic(Socket) ->
% Read data from the network socket and do our thang!
让我们继续完成它
实现一个通用网络服务器
使用gen_server来实现一个网络服务器的问题是,gen_tcp:accept调用是堵塞的
如果我们在服务器的初始化例程里调用它,那么整个gen_server机制都会堵塞,直到客户端建立连接
有两种方式来绕过这个问题
一种方式为使用低级连接机制来支持非堵塞(或异步)accept
有许多方法来支持这样做,最值得注意的是gen_tcp:controlling_process,它帮你管理当客户端建立连接时谁接受了什么消息
我认为另一种比较简单而更优雅的方式是,
一个单独的进程来监听socket
该进程做两件事:监听“接收连接”消息以及分配新的接收器
当它接收一条新的“接收连接”的消息时,就知道该分配新的接收器了
接收器可以任意调用堵塞的gen_tcp:accept,因为它允许在自己的进程里
当它接受一个连接后,它发出一条异步消息传回给父进程,并且立即调用业务逻辑方法
这里是代码,我加了一些注释,希望可读性还可以:
- -module(socket_server).
- -author('Jesse E.I. Farmer <[email protected]>').
- -behavior(gen_server).
-
- -export([init/1, code_change/3, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
- -export([accept_loop/1]).
- -export([start/3]).
-
- -define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]).
-
- -record(server_state, {
- port,
- loop,
- ip=any,
- lsocket=null}).
-
- start(Name, Port, Loop) ->
- State = #server_state{port = Port, loop = Loop},
- gen_server:start_link({local, Name}, ?MODULE, State, []).
-
- init(State = #server_state{port=Port}) ->
- case gen_tcp:listen(Port, ?TCP_OPTIONS) of
- {ok, LSocket} ->
- NewState = State#server_state{lsocket = LSocket},
- {ok, accept(NewState)};
- {error, Reason} ->
- {stop, Reason}
- end.
-
- handle_cast({accepted, _Pid}, State=#server_state{}) ->
- {noreply, accept(State)}.
-
- accept_loop({Server, LSocket, {M, F}}) ->
- {ok, Socket} = gen_tcp:accept(LSocket),
- % Let the server spawn a new process and replace this loop
- % with the echo loop, to avoid blocking
- gen_server:cast(Server, {accepted, self()}),
- M:F(Socket).
-
- % To be more robust we should be using spawn_link and trapping exits
- accept(State = #server_state{lsocket=LSocket, loop = Loop}) ->
- proc_lib:spawn(?MODULE, accept_loop, [{self(), LSocket, Loop}]),
- State.
-
- % These are just here to suppress warnings.
- handle_call(_Msg, _Caller, State) -> {noreply, State}.
- handle_info(_Msg, Library) -> {noreply, Library}.
- terminate(_Reason, _Library) -> ok.
- code_change(_OldVersion, Library, _Extra) -> {ok, Library}.
-module(socket_server).
-author('Jesse E.I. Farmer <[email protected]>').
-behavior(gen_server).
-export([init/1, code_change/3, handle_call/3, handle_cast/2, handle_info/2, terminate/2]).
-export([accept_loop/1]).
-export([start/3]).
-define(TCP_OPTIONS, [binary, {packet, 0}, {active, false}, {reuseaddr, true}]).
-record(server_state, {
port,
loop,
ip=any,
lsocket=null}).
start(Name, Port, Loop) ->
State = #server_state{port = Port, loop = Loop},
gen_server:start_link({local, Name}, ?MODULE, State, []).
init(State = #server_state{port=Port}) ->
case gen_tcp:listen(Port, ?TCP_OPTIONS) of
{ok, LSocket} ->
NewState = State#server_state{lsocket = LSocket},
{ok, accept(NewState)};
{error, Reason} ->
{stop, Reason}
end.
handle_cast({accepted, _Pid}, State=#server_state{}) ->
{noreply, accept(State)}.
accept_loop({Server, LSocket, {M, F}}) ->
{ok, Socket} = gen_tcp:accept(LSocket),
% Let the server spawn a new process and replace this loop
% with the echo loop, to avoid blocking
gen_server:cast(Server, {accepted, self()}),
M:F(Socket).
% To be more robust we should be using spawn_link and trapping exits
accept(State = #server_state{lsocket=LSocket, loop = Loop}) ->
proc_lib:spawn(?MODULE, accept_loop, [{self(), LSocket, Loop}]),
State.
% These are just here to suppress warnings.
handle_call(_Msg, _Caller, State) -> {noreply, State}.
handle_info(_Msg, Library) -> {noreply, Library}.
terminate(_Reason, _Library) -> ok.
code_change(_OldVersion, Library, _Extra) -> {ok, Library}.
我们使用gen_server:cast来传递异步消息给监听进程,当监听进程接受accepted消息后,它分配一个新的接收器
目前,这个服务器不是很健壮,因为如果无论什么原因活动的接收器失败以后,服务器会停止接收新的连接
为了让它变得更像OTP,我们因该捕获异常退出并且在连接失败时分配新的接收器
一个通用的echo服务器
echo服务器是最简单的服务器,让我们使用我们新的抽象socket服务器来写它:
- -module(echo_server).
- -author('Jesse E.I. Farmer <[email protected]>').
-
- -export([start/0, loop/1]).
-
- % echo_server specific code
- start() ->
- socket_server:start(?MODULE, 7000, {?MODULE, loop}).
- loop(Socket) ->
- case gen_tcp:recv(Socket, 0) of
- {ok, Data} ->
- gen_tcp:send(Socket, Data),
- loop(Socket);
- {error, closed} ->
- ok
- end.
-module(echo_server).
-author('Jesse E.I. Farmer <[email protected]>').
-export([start/0, loop/1]).
% echo_server specific code
start() ->
socket_server:start(?MODULE, 7000, {?MODULE, loop}).
loop(Socket) ->
case gen_tcp:recv(Socket, 0) of
{ok, Data} ->
gen_tcp:send(Socket, Data),
loop(Socket);
{error, closed} ->
ok
end.
你可以看到,服务器只含有自己的业务逻辑
连接处理被封装到socket_server里面
而这里的loop方法也和最初的echo服务器一样
希望你可以从中学到点什么,
我觉得我开始理解Erlang了
欢迎回复,特别关于是如何改进我的代码,cheers!