Permalink
Browse files

Initial commit

  • Loading branch information...
0 parents commit 71166af4203febfc525a3263f4db81dcb20c5602 @kevsmith committed Sep 15, 2009
@@ -0,0 +1,34 @@
+LIBDIR=`erl -eval 'io:format("~s~n", [code:lib_dir()])' -s init stop -noshell`
+PKGNAME=giza
+VERSION=0.1.0
+
+all: compile docs
+
+ebin:
+ mkdir ebin
+
+compile: ebin
+ cd src;erl -make
+ cp src/gen_nb_server.app ebin
+
+package: clean
+ mkdir ebin
+ @mkdir $(PKGNAME)-$(VERSION)/ && cp -rf ebin Makefile README.markdown src t $(PKGNAME)-$(VERSION)
+ @COPYFILE_DISABLE=true tar zcf $(PKGNAME)-$(VERSION).tgz $(PKGNAME)-$(VERSION)
+ @rm -rf $(PKGNAME)-$(VERSION)/
+
+doc:
+ mkdir -p doc
+
+docs: doc doc/*.html
+
+doc/*.html:
+ erl -eval 'edoc:files(["./src/gen_nb_server.erl"])' -noshell -s init stop
+ mv *.html erlang.png stylesheet.css edoc-info doc
+clean:
+ rm -f *.tgz *.tar.gz edoc-info *.html erlang.png erl_crash.dump
+ rm -rf $(PKGNAME)-$(VERSION)
+ rm -rf ebin
+ rm -rf doc
+ rm -f t/*.beam
+ rm -rf include
@@ -0,0 +1,70 @@
+## Description
+gen_nb_server is an OTP behavior designed to simplify writing completely non-blocking TCP servers. Gone are the
+days of having to use a separate process to perform the listen/accept loop. Instead, gen_nb_server uses features
+built into prim_inet to create a truly non-blocking server. This is the same mechanism networking-intense projects,
+like RabbitMQ, use to create their TCP servers.
+
+In addition, every gen_nb_server is also a gen_server so you can gen_server:call/cast/info to your heart's content!
+What's not to like!?!
+
+Coming soon:
+- Ability to dynamically add/remove listen ports
+- Possibily automate the creation of per-connection worker processes
+
+## How to use gen_nb_server
+
+1. Drop the gen_nb_server behavior annotation at the top of your source file like so:
+<pre>
+-behavior(gen_nb_server).
+</pre>
+
+2. Implement the required functions. These include the usual suspects from gen_server (see the gen_server man page
+for details) and two new functions: <code>sock_opts/0</code> amd <code>new_connection/2</code>.
+
+2a. <code>sock_opts/0</code> is used by gen_nb_server to retrieve the set of socket options to use when
+creating the listen socket. These options will also be inherited by the client connection sockets. See the manpages
+for gen_tcp and inet for more information on socket options.
+
+2b. <code>new_connection/2</code> is called every time a new connection is accepted. It is called with the newly
+connected socket and the server's current state.
+
+Here's a complete example which should give you an idea on how to use gen_nb_server:
+
+<pre>
+-module(example).
+
+-export([start_link/2]).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
+-export([terminate/2, sock_opts/0, new_connection/2]).
+
+-behavior(gen_nb_server).
+
+start_link(IpAddr, Port) ->
+ gen_nb_server:start_link(?MODULE, IpAddr, Port, []).
+
+init([]) ->
+ {ok, []}.
+
+handle_call(_Msg, _From, State) ->
+ {reply, ignored, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Msg, State) ->
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+sock_opts() ->
+ [binary, {active, once}, {packet, 0}].
+
+new_connection(Sock, State) ->
+ gen_tcp:send(Sock, list_to_binary(io_lib:format("No soup for you!~n", []))),
+ gen_tcp:close(Sock),
+ {ok, State}.
+</pre>
+
+Note: This code is also available in priv/example.
@@ -0,0 +1,10 @@
+all: compile
+
+ebin:
+ mkdir ebin
+
+compile: ebin
+ cd src;erl -pa ../../ebin -make
+
+clean:
+ rm -rf ebin
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,4 @@
+{"*", [warn_obsolete_guard, warn_unused_import,
+ warn_shadow_vars, warn_export_vars, debug_info,
+ {i, "../include"},
+ {outdir, "../ebin"}]}.
@@ -0,0 +1,54 @@
+%% Copyright (c) 2009 Hypothetical Labs, Inc.
+
+%% Permission is hereby granted, free of charge, to any person obtaining a copy
+%% of this software and associated documentation files (the "Software"), to deal
+%% in the Software without restriction, including without limitation the rights
+%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+%% copies of the Software, and to permit persons to whom the Software is
+%% furnished to do so, subject to the following conditions:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+%% THE SOFTWARE.
+
+-module(example).
+
+-export([start_link/2]).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2]).
+-export([terminate/2, sock_opts/0, new_connection/2]).
+
+-behavior(gen_nb_server).
+
+start_link(IpAddr, Port) ->
+ gen_nb_server:start_link(?MODULE, IpAddr, Port, []).
+
+init([]) ->
+ {ok, []}.
+
+handle_call(_Msg, _From, State) ->
+ {reply, ignored, State}.
+
+handle_cast(_Msg, State) ->
+ {noreply, State}.
+
+handle_info(_Msg, State) ->
+ {noreply, State}.
+
+terminate(_Reason, _State) ->
+ ok.
+
+sock_opts() ->
+ [binary, {active, once}, {packet, 0}].
+
+new_connection(Sock, State) ->
+ gen_tcp:send(Sock, list_to_binary(io_lib:format("No soup for you!~n", []))),
+ gen_tcp:close(Sock),
+ {ok, State}.
@@ -0,0 +1,4 @@
+{"*", [warn_obsolete_guard, warn_unused_import,
+ warn_shadow_vars, warn_export_vars, debug_info,
+ {i, "../include"},
+ {outdir, "../ebin"}]}.
No changes.
@@ -0,0 +1,164 @@
+%% Copyright (c) 2009 Hypothetical Labs, Inc.
+
+%% Permission is hereby granted, free of charge, to any person obtaining a copy
+%% of this software and associated documentation files (the "Software"), to deal
+%% in the Software without restriction, including without limitation the rights
+%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+%% copies of the Software, and to permit persons to whom the Software is
+%% furnished to do so, subject to the following conditions:
+%%
+%% The above copyright notice and this permission notice shall be included in
+%% all copies or substantial portions of the Software.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+%% THE SOFTWARE.
+
+-module(gen_nb_server).
+
+-author('kevin@hypotheticalabs.com').
+
+-behaviour(gen_server).
+
+%% API
+-export([start_link/4]).
+
+%% Behavior callbacks
+-export([behaviour_info/1]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
+ terminate/2, code_change/3]).
+
+-define(SERVER, ?MODULE).
+
+-record(state, {cb,
+ socks=[],
+ server_state}).
+
+%% @hidden
+behaviour_info(callbacks) ->
+ [{init, 1},
+ {handle_call, 3},
+ {handle_cast, 2},
+ {handle_info, 2},
+ {terminate, 2},
+ {sock_opts, 0},
+ {new_connection, 2}];
+
+behaviour_info(_) ->
+ undefined.
+
+%% @spec start_link(CallbackModule, IpAddr, Port, InitParams) -> Result
+%% CallbackModule = atom()
+%% IpAddr = string()
+%% Port = integer()
+%% InitParams = [any()]
+%% Result = {ok, pid()} | {error, any()}
+%% @doc Start server listening on IpAddr:Port
+start_link(CallbackModule, IpAddr, Port, InitParams) ->
+ gen_server:start_link(?MODULE, [CallbackModule, IpAddr, Port, InitParams], []).
+
+%% @hidden
+init([CallbackModule, IpAddr, Port, InitParams]) ->
+ case CallbackModule:init(InitParams) of
+ {ok, ServerState} ->
+ case listen_on(CallbackModule, IpAddr, Port) of
+ {ok, Sock} ->
+ {ok, #state{cb=CallbackModule, socks=[Sock], server_state=ServerState}};
+ Error ->
+ CallbackModule:terminate(Error, ServerState),
+ Error
+ end;
+ Err ->
+ Err
+ end.
+
+%% @hidden
+handle_call(Request, From, #state{cb=Callback, server_state=ServerState}=State) ->
+ case Callback:handle_call(Request, From, ServerState) of
+ {reply, Reply, NewServerState} ->
+ {reply, Reply, State#state{server_state=NewServerState}};
+ {reply, Reply, NewServerState, Arg} when Arg =:= hibernate orelse is_number(Arg) ->
+ {reply, Reply, State#state{server_state=NewServerState}, Arg};
+ {noreply, NewServerState} ->
+ {noreply, State#state{server_state=NewServerState}};
+ {noreply, NewServerState, Arg} when Arg =:= hibernate orelse is_number(Arg) ->
+ {noreply, State#state{server_state=NewServerState}, Arg};
+ {stop, Reason, NewServerState} ->
+ {stop, Reason, State#state{server_state=NewServerState}};
+ {stop, Reason, Reply, NewServerState} ->
+ {stop, Reason, Reply, State#state{server_state=NewServerState}}
+ end.
+
+%% @hidden
+handle_cast(Msg, #state{cb=Callback, server_state=ServerState}=State) ->
+ case Callback:handle_cast(Msg, ServerState) of
+ {noreply, NewServerState} ->
+ {noreply, State#state{server_state=NewServerState}};
+ {noreply, NewServerState, Arg} when Arg =:= hibernate orelse is_number(Arg) ->
+ {noreply, State#state{server_state=NewServerState}, Arg};
+ {stop, Reason, NewServerState} ->
+ {stop, Reason, State#state{server_state=NewServerState}}
+ end.
+
+%% @hidden
+handle_info({inet_async, ListSock, _Ref, {ok, CliSocket}}, #state{cb=Callback, server_state=ServerState}=State) ->
+ inet_db:register_socket(CliSocket, inet_tcp),
+ case Callback:new_connection(CliSocket, ServerState) of
+ {ok, NewServerState} ->
+ prim_inet:async_accept(ListSock, -1),
+ {noreply, State#state{server_state=NewServerState}};
+ {stop, Reason, NewServerState} ->
+ {stop, Reason, State#state{server_state=NewServerState}}
+ end;
+
+handle_info(Info, #state{cb=Callback, server_state=ServerState}=State) ->
+ case Callback:handle_info(Info, ServerState) of
+ {noreply, NewServerState} ->
+ {noreply, State#state{server_state=NewServerState}};
+ {noreply, NewServerState, Arg} when Arg =:= hibernate orelse is_number(Arg) ->
+ {noreply, State#state{server_state=NewServerState}, Arg};
+ {stop, Reason, NewServerState} ->
+ {stop, Reason, State#state{server_state=NewServerState}}
+ end.
+
+%% @hidden
+terminate(Reason, #state{cb=Callback, server_state=ServerState}) ->
+ Callback:terminate(Reason, ServerState),
+ ok.
+
+%% @hidden
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+%% Internal functions
+
+%% @hidden
+%% @spec listen_on(CallbackModule, IpAddr, Port) -> Result
+%% CallbackModule = atom()
+%% IpAddr = string()
+%% Port = integer()
+%% Result = {ok, port()} | {error, any()}
+listen_on(CallbackModule, IpAddr, Port) ->
+ SockOpts = [{ip, convert(IpAddr)}|CallbackModule:sock_opts()],
+ case gen_tcp:listen(Port, SockOpts) of
+ {ok, LSock} ->
+ {ok, _Ref} = prim_inet:async_accept(LSock, -1),
+ {ok, LSock};
+ Err ->
+ Err
+ end.
+
+%% @hidden
+%% @spec convert_addr(Addr) -> Result
+%% Addr = string()
+%% Result = {integer(), integer(), integer(), integer()}
+%% @doc Converts text IP addresses "0.0.0.0" to tuples {0, 0, 0, 0}
+convert(Addr) ->
+ T = string:tokens(Addr, "."),
+ list_to_tuple([list_to_integer(X) || X <- T]).

0 comments on commit 71166af

Please sign in to comment.