Skip to content
Browse files

version 1.0: basic functionality

Basic functionality:
* acceptors pool;
* binding callback to TCP-port;
* receive with buffering;
* full support of sockopts, including 'active' option.
*
  • Loading branch information...
1 parent 7d9e5db commit 77ba5f347bd3b5c61865dbea8976188482644b4b @seletskiy committed May 11, 2012
View
3 .gitignore
@@ -0,0 +1,3 @@
+doc
+ebin
+.eunit
View
12 README.md
@@ -1,4 +1,14 @@
gen_tcpd
========
-Generic TCP application, makes easy to build TCP-daemons.
+Generic TCP application, makes easy to build TCP-daemons.
+
+Ten lines of trivial code and here you go, simple
+echo client is ready: `src/gen_tcpd_handler_example_echo.erl`.
+
+Documentation
+-------------
+
+`gen_tcpd` uses basho/rebar as build system, so
+use `rebar co doc` to compile and create documentation.
+After that documentation can be found in `doc/` directory.
View
2 rebar.config
@@ -0,0 +1,2 @@
+% ex: ft=erlang
+{erl_opts, [no_debug_info, {i, "include"}, bin_opt_info]}.
View
11 src/gen_tcpd.app.src
@@ -0,0 +1,11 @@
+% ex: ft=erlang
+{application, gen_tcpd, [
+ {description, "Generic TCP daemon."},
+ {vsn, git},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib,
+ sasl]},
+ {mod, {gen_tcpd, []}},
+ {env, []}]}.
View
73 src/gen_tcpd.erl
@@ -0,0 +1,73 @@
+%% @author Stanislav Seletskiy <s.seletskiy@office.ngs.ru>
+%% @doc gen_tcpd application.
+%% Makes easy to create TCP daemons.
+%% Should be started with `application:start(gen_tcpd)'.
+-module(gen_tcpd).
+-created('Date: 27/04/2012').
+-created_by('Stanislav Seletskiy <s.seletskiy@office.ngs.ru>').
+-behaviour(application).
+
+-export([start/2, stop/1]).
+-export([bind/2, bind/3, unbind/1]).
+
+%% ---------------------------------------------------------------------
+%% Public methods (открытые методы).
+%% ---------------------------------------------------------------------
+
+%% @doc Starts an application.
+start(_Type, _Args) ->
+ gen_tcpd_sup:start_link().
+
+%% @doc Stops an application.
+stop(_State) ->
+ ok.
+
+%% @doc Same as `bind/2' with empty `Options' list.
+%% @see bind/3
+bind(Port, MFA) ->
+ bind(Port, MFA, []).
+
+%% @doc Binds callback module to specified port.
+%% This method creates listen socket and distributes it on
+%% acceptors pool (default - 5 workers).
+%%
+%% `MFA' will be called after succeeded connection on `Port'.
+%%
+%% `MFA' must return `{ok, State}'.
+%%
+%% `MFA' must have behaviour `gen_tcpd_handler_behaviour'.
+%%
+%% See `gen_tcpd_handler_example_echo' to learn how to write simple
+%% callback module.
+%%
+%% See `gen_tcpd_conn' to learn how to control socket connection.
+%%
+%% `Options' is a prolist with keys:
+%% <ul>
+%% <li>`pool' describes acceptors pool options:
+%% <ul>
+%% <li>`workers' - workers count (default - 5);</li>
+%% <li>`max_restarts' - maximum restarts
+%% in `restarts_time' (default - 5);</li>
+%% <li>`restarts_time' - maximum interval
+%% in seconds for `max_restarts' (default - 60);</li>
+%% </ul>
+%% </li>
+%% <li>`listener' describes listen socket options:
+%% <ul>
+%% <li>all options from `inet:setopts()' except `active'.</li>
+%% </ul>
+%% </li>
+%% </ul>
+%% @see gen_tcpd_handler_behaviour
+%% @see gen_tcpd_handler_example_echo
+-spec bind(integer(), mfa(), list()) -> {ok, pid()}.
+bind(Port, MFA, Options) ->
+ supervisor:start_child(gen_tcpd_pools_sup,
+ [Port, MFA, Options]).
+
+%% @doc Remove binding from `Port'.
+-spec unbind(integer()) -> ok.
+unbind(Port) ->
+ supervisor:terminate_child(gen_tcpd_pools_sup,
+ whereis(gen_tcpd_pools_sup:make_child_name(Port))).
View
25 src/gen_tcpd_acceptor.erl
@@ -0,0 +1,25 @@
+%% @author Stanislav Seletskiy <s.seletskiy@office.ngs.ru>
+%% @doc Acceptor module, accepts incoming conections for
+%% already created socket.
+-module(gen_tcpd_acceptor).
+-created('Date: 27/04/2012').
+-created_by('Stanislav Seletskiy <s.seletskiy@office.ngs.ru>').
+
+-define(ACCEPT_TIMEOUT, 1000).
+
+-export([start_link/3]).
+-export([bind/3]).
+
+start_link(Socket, SockOptions, MFA) ->
+ {ok, spawn_link(?MODULE, bind, [Socket, SockOptions, MFA])}.
+
+bind(ListenSocket, SockOptions, MFA) ->
+ case gen_tcp:accept(ListenSocket, ?ACCEPT_TIMEOUT) of
+ {ok, Socket} ->
+ {ok, Pid} = supervisor:start_child(gen_tcpd_conns_sup, [Socket, MFA]),
+ gen_tcp:controlling_process(Socket, Pid),
+ inet:setopts(Socket, SockOptions),
+ bind(ListenSocket, SockOptions, MFA);
+ {error, timeout} ->
+ bind(ListenSocket, SockOptions, MFA)
+ end.
View
161 src/gen_tcpd_conn.erl
@@ -0,0 +1,161 @@
+%% @author Stanislav Seletskiy <s.seletskiy@office.ngs.ru>
+%% @doc Module implements 2 roles: recv and send.
+%% <ol>
+%% <li>it receives all messages, that are sended by connected client;</li>
+%% <li>it sends messages to connected client that are specified in {@link send/2}.</li>
+%% </ol>
+%%
+%% Socket opens with `{active, false}' option, so you need manually to
+%% set it to be able to accept incoming messages. It can be done
+%% with `activate/1' and `wait_data/2' functions.
+%%
+%% See `gen_tcpd_handler_example_echo' for example usage.
+%%
+%% @see activate/1
+%% @see activate/2
+%% @see gen_tcpd_handler_example_echo
+-module(gen_tcpd_conn).
+
+-behaviour(gen_server).
+
+-export([
+ send/2,
+ activate/1,
+ activate/2,
+ close/1,
+ start_link/2,
+ init/1,
+ handle_call/3,
+ handle_cast/2,
+ handle_info/2,
+ terminate/2,
+ code_change/3]).
+
+-record(state, {
+ sock :: gen_tcp:socket(),
+ module :: module(),
+ module_state :: any(),
+ buffer_pid :: pid()}).
+
+%% ---------------------------------------------------------------------
+%% Public methods (открытые методы).
+%% ---------------------------------------------------------------------
+
+%% @doc Start connection module and spawn user defined `Module'.
+-spec start_link(gen_tcp:socket(), mfa()) -> {ok, pid()}.
+start_link(Socket, MFA) ->
+ gen_server:start_link(?MODULE, [Socket, MFA], []).
+
+
+%% @doc Sends `Data' to socket, linked to connection with pid `Pid'.
+-spec send(pid(), list()) -> ok.
+send(Pid, Data) ->
+ gen_server:cast(Pid, {send, Data}).
+
+%% @doc Switch socket to active state.
+%% `recv/2' from callback module will be callbed,
+%% when some data arrived at socket.
+-spec activate(pid()) -> ok.
+activate(Pid) ->
+ activate(Pid, 0).
+
+%% @doc Switches socket to active state in buffered mode.
+%% `recv/2' from callback module will be called only after
+%% `BufferSize' bytes was arrived at socket.
+activate(Pid, BufferSize) ->
+ gen_server:cast(Pid, {activate, BufferSize}).
+
+%% @doc Closes connection.
+close(Pid) ->
+ gen_server:cast(Pid, close).
+
+%% ---------------------------------------------------------------------
+%% Private methods (закрытые методы).
+%% ---------------------------------------------------------------------
+
+recv_data(Socket, 0) ->
+ inet:setopts(Socket, [{active, once}]),
+ undefined;
+
+recv_data(Socket, BufferSize) ->
+ {BufferPid, _} = gen_tcpd_conn_buffer:start_monitor(
+ Socket, self(), BufferSize),
+ BufferPid.
+
+%% ---------------------------------------------------------------------
+%% gen_server specific.
+%% ---------------------------------------------------------------------
+
+%% @private
+init([Socket, _MFA = {Module, Function, Args}]) ->
+ {ok, ModState} = apply(Module, Function, [self()] ++ Args),
+ {ok, #state{
+ sock = Socket,
+ module = Module,
+ module_state = ModState}}.
+
+%% @private
+handle_call(_Message, _From, State) ->
+ {noreply, State}.
+
+%% @private
+handle_cast({activate, BufferSize}, State = #state{buffer_pid = undefined}) ->
+ #state{sock = Socket} = State,
+ BufferPid = recv_data(Socket, BufferSize),
+ {noreply, State#state{
+ buffer_pid = BufferPid}};
+
+%% @private
+handle_cast({activate, _}, State) ->
+ {noreply, State};
+
+%% @private
+handle_cast({send, Data}, State) ->
+ #state{sock = Socket} = State,
+ gen_tcp:send(Socket, Data),
+ {noreply, State};
+
+handle_cast(close, State) ->
+ #state{sock = Socket} = State,
+ gen_tcp:close(Socket),
+ {stop, normal, State};
+
+%% @private
+handle_cast(_Message, State) ->
+ {noreply, State}.
+
+%% @private
+handle_info({tcp, _Socket, Data}, State) ->
+ #state{
+ module = Module,
+ module_state = ModState} = State,
+ NewModState = Module:recv(ModState, Data),
+ {noreply, State#state{module_state = NewModState}};
+
+%% @private
+handle_info({tcp_closed, _Socket}, State) ->
+ {stop, normal, State};
+
+%% @private
+handle_info({'DOWN', _, _, _, normal}, State) ->
+ {noreply, State#state{buffer_pid = undefined}};
+
+%% @private
+handle_info({'DOWN', _, _, _, _}, State) ->
+ {stop, normal, State};
+
+%% @private
+handle_info(_Info, State) ->
+ {noreply, State}.
+
+%% @private
+terminate(_Reason, State) ->
+ #state{
+ module = Module,
+ module_state = ModState} = State,
+ Module:stop(ModState),
+ ok.
+
+%% @private
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
View
31 src/gen_tcpd_conn_buffer.erl
@@ -0,0 +1,31 @@
+%% @author Stanislav Seletskiy <s.seletskiy@office.ngs.ru>
+%% @doc Buffer module for buffered read mode (by packet).
+-module(gen_tcpd_conn_buffer).
+-created('Date: 02/05/2012').
+-created_by('Stanislav Seletskiy <s.seletskiy@office.ngs.ru>').
+
+-export([start_monitor/3, read/3]).
+
+%% ---------------------------------------------------------------------
+%% Public methods (открытые методы).
+%% ---------------------------------------------------------------------
+
+-spec start_monitor(port(), pid(), integer()) -> {ok, pid()}.
+start_monitor(Socket, ConnPid, Size) ->
+ spawn_monitor(?MODULE, read, [Socket, ConnPid, Size]).
+
+%% ---------------------------------------------------------------------
+%% Private methods (закрытые методы).
+%% ---------------------------------------------------------------------
+
+%% @private
+read(Socket, ConnPid, Size) ->
+ {ok, [{packet, Packet}]} = inet:getopts(Socket, [packet]),
+ ok = inet:setopts(Socket, [{packet, raw}]),
+ case gen_tcp:recv(Socket, Size) of
+ {ok, Data} ->
+ ok = inet:setopts(Socket, [{packet, Packet}]),
+ ConnPid ! {tcp, Socket, Data};
+ {error, closed} ->
+ ConnPid ! {tcp_closed, Socket}
+ end.
View
22 src/gen_tcpd_conns_sup.erl
@@ -0,0 +1,22 @@
+%% @author Stanislav Seletskiy <s.seletskiy@office.ngs.ru>
+%% @doc Supervisor for connections.
+-module(gen_tcpd_conns_sup).
+-created('Date: 27/04/2012').
+-created_by('Stanislav Seletskiy <s.seletskiy@office.ngs.ru>').
+
+-behaviour(supervisor).
+
+-export([start_link/0, init/1]).
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init(_Args) ->
+ {ok, {{simple_one_for_one, 5, 60}, [
+ {gen_tcpd_conn,
+ {gen_tcpd_conn, start_link, []},
+ transient,
+ 5000,
+ worker,
+ [gen_tcpd_conn]}
+ ]}}.
View
22 src/gen_tcpd_handler_behaviour.erl
@@ -0,0 +1,22 @@
+%% @author Stanislav Seletskiy <s.seletskiy@office.ngs.ru>
+%% @doc Behaviour for callback module of gen_tcpd.
+%% Behaviour ensures two methods:
+%% <ul>
+%% <li>`recv(ModulePid, Data)' will be called, when data
+%% was received from socket.</li>
+%% `ModulePid' is a pid, returned from `MFA' function in `gen_tcpd:bind()' call.
+%% <li>`stop(ModulePid)' will be called, when socket closes
+%% or on system shutdown</li>
+%% </ul>
+-module(gen_tcpd_handler_behaviour).
+-created('Date: 28/04/2012').
+-created_by('Stanislav Seletskiy <s.seletskiy@office.ngs.ru>').
+
+-export([behaviour_info/1]).
+
+behaviour_info(callbacks) ->
+ [{recv, 2}, {stop, 1}];
+
+behaviour_info(_) ->
+ undefined.
+
View
43 src/gen_tcpd_handler_example_echo.erl
@@ -0,0 +1,43 @@
+%% @author Stanislav Seletskiy <s.seletskiy@office.ngs.ru>
+%% @doc Example echo client for gen_tcpd.
+%% Module starts with `start/1`.
+%%
+%% To start module from REPL (`12345' is an example port):
+%%
+%% <code>
+%% gen_tcpd_handler_example_echo:start(12345).
+%% </code>
+-module(gen_tcpd_handler_example_echo).
+-created('Date: 28/04/2012').
+-created_by('Stanislav Seletskiy <s.seletskiy@office.ngs.ru>').
+
+-behaviour(gen_tcpd_handler_behaviour).
+
+-export([start/1, init/1, recv/2, stop/1]).
+
+start(Port) ->
+ gen_tcpd:bind(
+ Port, {?MODULE, init, []},
+ [{listener, [
+ {active, true},
+ {reuseaddr, true}]}]).
+
+%% @doc Starts module.
+%%
+%% `ConnPid' is a pid of `gen_tcpd_conn' instance.
+%%
+%% @see gen_tcpd_conn
+-spec init(pid()) -> {ok, pid()}.
+init(ConnPid) ->
+ {ok, ConnPid}.
+
+%% @doc Will be called when some data received by socket.
+-spec recv(pid(), list()) -> {pid()}.
+recv(State = ConnPid, Data) ->
+ gen_tcpd_conn:send(ConnPid, Data),
+ State.
+
+%% @doc Will be called on socket close or system shutdown.
+-spec stop(any()) -> ok.
+stop(_State) ->
+ ok.
View
13 src/gen_tcpd_listener.erl
@@ -0,0 +1,13 @@
+%% @author Stanislav Seletskiy <s.seletskiy@office.ngs.ru>
+%% @doc Listener module, creates listener socket.
+%% Does not accept incoming connections.
+-module(gen_tcpd_listener).
+-created('Date: 27/04/2012').
+-created_by('Stanislav Seletskiy <s.seletskiy@office.ngs.ru>').
+
+-export([listen/2]).
+
+listen(Port, SockOptions) ->
+ {ok, Socket} = gen_tcp:listen(Port, [{active, false}] ++ SockOptions),
+ Socket.
+
View
46 src/gen_tcpd_pool_sup.erl
@@ -0,0 +1,46 @@
+%% @author Stanislav Seletskiy <s.seletskiy@office.ngs.ru>
+%% @doc Supervisor for acceptors pool (one port).
+-module(gen_tcpd_pool_sup).
+-created('Date: 27/04/2012').
+-created_by('Stanislav Seletskiy <s.seletskiy@office.ngs.ru>').
+
+-define(DEFAULT_MAX_R, 5).
+-define(DEFAULT_MAX_T, 60).
+-define(DEFAULT_WORKERS, 5).
+
+-behaviour(supervisor).
+
+-export([start_link/3, init/1]).
+
+%% ---------------------------------------------------------------------
+%% Public methods (открытые методы).
+%% ---------------------------------------------------------------------
+start_link(Port, MFA, Options) ->
+ supervisor:start_link(
+ {local, gen_tcpd_pools_sup:make_child_name(Port)},
+ ?MODULE, [Port, MFA, Options]).
+
+init([Port, MFA, Options]) ->
+ PoolOptions = proplists:get_value(pool, Options, []),
+ ListenerOptions = proplists:get_value(listener, Options, []),
+ MaxRestarts = proplists:get_value(max_restarts, PoolOptions, ?DEFAULT_MAX_R),
+ RestartsTime = proplists:get_value(restarts_time, PoolOptions, ?DEFAULT_MAX_T),
+ WorkersCount = proplists:get_value(workers, PoolOptions, ?DEFAULT_WORKERS),
+ Socket = gen_tcpd_listener:listen(Port, ListenerOptions),
+ {ok, {{one_for_one, MaxRestarts, RestartsTime},
+ lists:map(fun(Index) ->
+ create_child_spec(Index, [Socket, ListenerOptions, MFA]) end,
+ lists:seq(1, WorkersCount))
+ }}.
+
+
+%% ---------------------------------------------------------------------
+%% Private methods (закрытые методы).
+%% ---------------------------------------------------------------------
+create_child_spec(Index, Args) ->
+ {list_to_atom("gen_tcpd_listener+" ++ integer_to_list(Index)),
+ {gen_tcpd_acceptor, start_link, Args},
+ permanent,
+ 5000,
+ worker,
+ [gen_tcpd_listener]}.
View
26 src/gen_tcpd_pools_sup.erl
@@ -0,0 +1,26 @@
+%% @author Stanislav Seletskiy <s.seletskiy@office.ngs.ru>
+%% @doc Supervisor for listener processes.
+-module(gen_tcpd_pools_sup).
+-created('Date: 27/04/2012').
+-created_by('Stanislav Seletskiy <s.seletskiy@office.ngs.ru>').
+
+-behaviour(supervisor).
+
+-export([start_link/0, init/1]).
+-export([make_child_name/1]).
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init(_Args) ->
+ {ok, {{simple_one_for_one, 5, 60}, [
+ {gen_tcpd_pool_sup,
+ {gen_tcpd_pool_sup, start_link, []},
+ transient,
+ 5000,
+ worker,
+ [gen_tcpd_pool_sup]}
+ ]}}.
+
+make_child_name(Port) ->
+ list_to_atom("gen_tcpd_pool_sup+" ++ integer_to_list(Port)).
View
28 src/gen_tcpd_sup.erl
@@ -0,0 +1,28 @@
+%% @author Stanislav Seletskiy <s.seletskiy@office.ngs.ru>
+%% @doc Main supervisor for gen_tcpd application.
+-module(gen_tcpd_sup).
+-created('Date: 27/04/2012').
+-created_by('Stanislav Seletskiy <s.seletskiy@office.ngs.ru>').
+
+-behaviour(supervisor).
+
+-export([start_link/0, init/1]).
+
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+init(_Args) ->
+ {ok, {{one_for_one, 5, 60}, [
+ {gen_tcpd_pools_sup,
+ {gen_tcpd_pools_sup, start_link, []},
+ permanent,
+ 5000,
+ worker,
+ [gen_tcpd_pools_sup]},
+ {gen_tcpd_conns_sup,
+ {gen_tcpd_conns_sup, start_link, []},
+ permanent,
+ 5000,
+ worker,
+ [gen_tcpd_conns_sup]}
+ ]}}.

0 comments on commit 77ba5f3

Please sign in to comment.
Something went wrong with that request. Please try again.