这两天在研究 erlang 如何构建 TCP 服务器,看到一篇文章,基于Erlang OTP构建一个TCP服务器,里面讲述了两种混合型Socket的实现方法,着实让人欢欣鼓舞。对比老外写的Building a Non-blocking TCP server using OTP principles,作者写的那个有点简单。本文将结合这两篇文章,继续讨论Erlang/OTP 构建TCP服务器的具体实现,以示例演示如何如何使用标准Erlang/OTP行为创建一个简单的无阻塞的TCP服务器。
TCP Socket模式
主动模式{active, true},非阻塞方式接收消息,但在系统无法应对超大流量请求时,客户端发送的数据过快,而且超过服务器可以处理的速度,那么,系统就可能会造成消息缓冲区被塞满,出现持续繁忙的流量的极端情况,系统因请求过多而溢出,造成Erlang虚拟机内存不足而崩溃。
被动模式{active, false},阻塞方式接收消息,底层的TCP缓冲区可用于抑制请求,并拒绝客户端的消息,在接收数据的地方都会调用gen_tcp:recv,造成阻塞(单进程模式下就只能消极等待某一个具体的客户端Socket ,很危险)。需要注意的是,操作系统可能还会做一些缓存允许客户端机器继续发送少量数据,然后才将其阻塞,但这个时候Erlang还没有调用recv函数。
混合型模式(半阻塞,{active, once}),主动套接字仅针对一条消息,在控制进程发送完一个消息数据后,必须显式地调用inet:setopts(Socket, [{active, once}]) 重新激活以便接受下一个消息(在此之前,系统处于阻塞状态)。可见,混合型模式综合了主动模式和被动模式的两者优势,可实现流量控制,防止服务器被过多消息淹没。
所以如果想构建TCP服务器,比较合理的是建立在TCP Socket 混合型模式(半阻塞)基础上。
TCP服务器设计
这个TCP服务器的设计包含了主应用程序 tcp_server_app 和监督者 tcp_server_sup 进程,监督者进程拥有 tcp_server_listener 和 tcp_client_sup 两个子进程。tcp_server_listener 负责处理客户端的连接请求,并通知 tcp_client_sup 启动一个 tcp_server_handler 实例进程来处理一条客户端的请求,然后由该实例进程负责处理服务器与客户端的交互数据。
应用程序和监督行为
为了构建一个 Erlang/OTP 应用程序,我们需要构建一些模块来实现应用程序和监督行为。当应用程序启动时,tcp_server_app:start/2 会调用 tcp_server_sup:start_link/1 来创建主监督进程。该监督进程通过回调 tcp_server_sup:init/1 来实例化子工作进程 tcp_server_listener 和子监督进程 tcp_client_sup。该子监督进程回调 tcp_server_sup:init/1 来实例化负责处理客户端连接的工作进程 tcp_server_handler。
TCP服务器应用程序 (tcp_server_app.erl)
[plain] view plain copy
1. -module(tcp_server_app).
2. -behaviour(application).
3. -export([start/2, stop/1]).
4. -define(PORT, 2222).
5.
6. start(_Type, _Args) ->
7. io:format("tcp app start~n"),
8. case tcp_server_sup:start_link(?PORT) of
9. {ok, Pid} ->
10. {ok, Pid};
11. Other ->
12. {error, Other}
13. end.
14.
15. stop(_S) ->
16. ok.
TCP服务器监督者进程(tcp_server_sup.erl)
[plain] view plain copy
1. -module(tcp_server_sup).
2. -behaviour(supervisor).
3. -export([start_link/1, start_child/1]).
4. -export([init/1]).
5.
6. start_link(Port) ->
7. io:format("tcp sup start link~n"),
8. supervisor:start_link({local, ?MODULE}, ?MODULE, [Port]).
9.
10. start_child(LSock) ->
11. io:format("tcp sup start child~n"),
12. supervisor:start_child(tcp_client_sup, [LSock]).
13.
14. init([tcp_client_sup]) ->
15. io:format("tcp sup init client~n"),
16. {ok,
17. { {simple_one_for_one, 0, 1},
18. [
19. { tcp_server_handler,
20. {tcp_server_handler, start_link, []},
21. temporary,
22. brutal_kill,
23. worker,
24. [tcp_server_handler]
25. }
26. ]
27. }
28. };
29.
30. init([Port]) ->
31. io:format("tcp sup init~n"),
32. {ok,
33. { {one_for_one, 5, 60},
34. [
35. % client supervisor
36. { tcp_client_sup,
37. {supervisor, start_link, [{local, tcp_client_sup}, ?MODULE, [tcp_client_sup]]},
38. permanent,
39. 2000,
40. supervisor,
41. [tcp_server_listener]
42. },
43. % tcp listener
44. { tcp_server_listener,
45. {tcp_server_listener, start_link, [Port]},
46. permanent,
47. 2000,
48. worker,
49. [tcp_server_listener]
50. }
51. ]
52. }
53. }.
TCP服务器 Socket 监听进程(tcp_server_listener.erl)
[plain] view plain copy
1. -module(tcp_server_listener).
2. -behaviour(gen_server).
3. -export([start_link/1]).
4. -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
5. terminate/2, code_change/3]).
6. -record(state, {lsock}).
7.
8. start_link(Port) ->
9. io:format("tcp server listener start ~n"),
10. gen_server:start_link({local, ?MODULE}, ?MODULE, [Port], []).
11.
12. init([Port]) ->
13. process_flag(trap_exit, true),
14. Opts = [binary, {packet, 0}, {reuseaddr, true},
15. {keepalive, true}, {backlog, 30}, {active, false}],
16. State =
17. case gen_tcp:listen(Port, Opts) of
18. {ok, LSock} ->
19. start_server_listener(LSock),
20. #state{lsock = LSock};
21. _Other ->
22. throw({error, {could_not_listen_on_port, Port}}),
23. #state{}
24. end,
25. {ok, State}.
26.
27. handle_call(_Request, _From, State) ->
28. io:format("tcp server listener call ~p~n", [_Request]),
29. {reply, ok, State}.
30.
31. handle_cast({tcp_accept, Pid}, State) ->
32. io:format("tcp server listener cast ~p~n", [tcp_accept]),
33. start_server_listener(State, Pid),
34. {noreply, State};
35.
36. handle_cast(_Msg, State) ->
37. io:format("tcp server listener cast ~p~n", [_Msg]),
38. {noreply, State}.
39.
40. handle_info({'EXIT', Pid, _}, State) ->
41. io:format("tcp server listener info exit ~p~n", [Pid]),
42. start_server_listener(State, Pid),
43. {noreply, State};
44.
45. handle_info(_Info, State) ->
46. io:format("tcp server listener info ~p~n", [_Info]),
47. {noreply, State}.
48.
49. terminate(_Reason, _State) ->
50. io:format("tcp server listener terminate ~p~n", [_Reason]),
51. ok.
52.
53. code_change(_OldVsn, State, _Extra) ->
54. {ok, State}.
55.
56. start_server_listener(State, Pid) ->
57. unlink(Pid),
58. start_server_listener(State#state.lsock).
59.
60. start_server_listener(Lsock) ->
61. case tcp_server_sup:start_child(Lsock) of
62. {ok, Pid} ->
63. link(Pid);
64. _Other ->
65. do_log
66. end.
TCP服务器处理客户端请求进程(tcp_server_handler.erl)
[plain] view plain copy
1. -module(tcp_server_handler).
2. -behaviour(gen_server).
3. -export([start_link/1]).
4. -export([init/1, handle_call/3, handle_cast/2, handle_info/2,
5. terminate/2, code_change/3]).
6. -record(state, {lsock, socket, addr}).
7. -define(Timeout, 120*1000).
8.
9. start_link(LSock) ->
10. io:format("tcp handler start link~n"),
11. gen_server:start_link(?MODULE, [LSock], []).
12.
13. init([LSock]) ->
14. io:format("tcp handler init ~n"),
15. inet:setopts(LSock, [{active, once}]),
16. gen_server:cast(self(), tcp_accept),
17. {ok, #state{lsock = LSock}}.
18.
19. handle_call(Msg, _From, State) ->
20. io:format("tcp handler call ~p~n", [Msg]),
21. {reply, {ok, Msg}, State}.
22.
23. handle_cast(tcp_accept, #state{lsock = LSock} = State) ->
24. {ok, CSock} = gen_tcp:accept(LSock),
25. io:format("tcp handler info accept client ~p~n", [CSock]),
26. {ok, {IP, _Port}} = inet:peername(CSock),
27. start_server_listener(self()),
28. {noreply, State#state{socket=CSock, addr=IP}, ?Timeout};
29.
30. handle_cast(stop, State) ->
31. {stop, normal, State}.
32.
33. handle_info({tcp, Socket, Data}, State) ->
34. inet:setopts(Socket, [{active, once}]),
35. io:format("tcp handler info ~p got message ~p~n", [self(), Data]),
36. ok = gen_tcp:send(Socket, <<Data/binary>>),
37. {noreply, State, ?Timeout};
38.
39. handle_info({tcp_closed, _Socket}, #state{addr=Addr} = State) ->
40. io:format("tcp handler info ~p client ~p disconnected~n", [self(), Addr]),
41. {stop, normal, State};
42.
43. handle_info(timeout, State) ->
44. io:format("tcp handler info ~p client connection timeout~n", [self()]),
45. {stop, normal, State};
46.
47. handle_info(_Info, State) ->
48. io:format("tcp handler info ingore ~p~n", [_Info]),
49. {noreply, State}.
50.
51. terminate(_Reason, #state{socket=Socket}) ->
52. io:format("tcp handler terminate ~p~n", [_Reason]),
53. (catch gen_tcp:close(Socket)),
54. ok.
55.
56. code_change(_OldVsn, State, _Extra) ->
57. {ok, State}.
58.
59. start_server_listener(Pid) ->
60. gen_server:cast(tcp_server_listener, {tcp_accept, Pid}).
TCP服务器资源文件(tcp_server.app)
[plain] view plain copy
1. {application,tcp_server,
2. [{description,"TCP Server"},
3. {vsn,"1.0.0"},
4. {modules,[tcp_server,tcp_server_app,tcp_server_handler,
5. tcp_server_listener,tcp_server_sup]},
6. {registered,[]},
7. {mod,{tcp_server_app,[]}},
8. {env,[]},
9. {applications,[kernel,stdlib]}]}.
编译程序
为应用程序创建如下的目录结构:
[plain] view plain copy
1. ./tcp_server
2. ./tcp_server/ebin/
3. ./tcp_server/ebin/tcp_server.app
4. ./tcp_server/src/tcp_server_app.erl
5. ./tcp_server/src/tcp_server_sup.erl
6. ./tcp_server/src/tcp_server_listener.erl
7. ./tcp_server/src/tcp_server_handler.erl
Linux:
[plain] view plain copy
1. $ cd tcp_server/src
2. $ for f in tcp*.erl ; do erlc -o ../ebin $f
Windows:
[plain] view plain copy
1. cd tcp_server/src
2. for %%i in (tcp*.erl) do erlc -o ../ebin %%i
运行程序
1、启动TCP服务器
[plain] view plain copy
1. erl -pa ebin
2. ...
3. 1> application:start(tcp_server).
4. tcp app start
5. tcp sup start link
6. tcp sup init
7. tcp sup init client
8. tcp server listener start
9. tcp sup start child
10. tcp handler start link
11. tcp handler init
12. ok
13. 2> appmon:start().
14. {ok,<0.41.0>}
2、创建一个客户端来请求TCP服务器:
[plain] view plain copy
1. 3> f(S), {ok,S} = gen_tcp:connect({127,0,0,1},2222,[{packet,0}]).
2. {ok,#Port<0.1859>}
3. tcp handler info accept client #Port<0.1860>
4. tcp server listener cast tcp_accept
5. tcp sup start child
6. tcp handler start link
7. tcp handler init
3、使用该请求向服务端发送消息:
[plain] view plain copy
1. 4> gen_tcp:send(S,<<"hello">>).
2. ok
3. tcp handler info <0.53.0> got message <<"hello">>
4、接收到服务端发来的信息:
[plain] view plain copy
1. 5> f(M), receive M -> M end.
2. {tcp,#Port<0.1861>,"hello"}
5、现在让我们尝试向服务端发送多个连接请求:
[plain] view plain copy
1. 6> gen_tcp:connect({127,0,0,1},2222,[{packet,0}]).
2. ...
3. 7> gen_tcp:connect({127,0,0,1},2222,[{packet,0}]).
4. ...
5. 8> gen_tcp:connect({127,0,0,1},2222,[{packet,0}]).
6. ...
6、服务端程序写有超时功能,如果2分钟内没操作,连接将自动退出
[plain] view plain copy
1. 9> tcp handler info <0.39.0> client connection timeout
2. 9> tcp handler terminate normal
3. 9> tcp handler info <0.52.0> client connection timeout
4. 9> tcp handler terminate normal
5. 9> tcp handler info <0.54.0> client connection timeout
6. 9> tcp handler terminate normal
7. 9> tcp handler info <0.56.0> client connection timeout
8. 9> tcp handler terminate normal
7、下面我们简单演示一下服务器的监督行为:
[plain] view plain copy
1. 9> exit(pid(0,58,0),kill).
2. tcp server listener info exit <0.58.0>
3. true
4. tcp sup start child
5. tcp handler start link
6. tcp handler init