diff options
41 files changed, 1272 insertions, 137 deletions
@@ -10,7 +10,7 @@ dep_ct_helper = https://github.com/extend/ct_helper.git master # Options. COMPILE_FIRST = ranch_transport -CT_SUITES = acceptor sendfile +CT_SUITES = acceptor sendfile shutdown PLT_APPS = crypto public_key ssl # Standard targets. @@ -22,9 +22,9 @@ to close any of the currently opened sockets. Getting started --------------- - * [Read the guide](http://ninenines.eu/docs/en/ranch/HEAD/guide/introduction) + * [Read the guide](http://ninenines.eu/docs/en/ranch/HEAD/guide) + * [Check the manual](http://ninenines.eu/docs/en/ranch/HEAD/manual) * Look at the examples in the `examples/` directory - * Build API documentation with `make docs`; open `doc/index.html` Support ------- diff --git a/doc/overview.edoc b/doc/overview.edoc deleted file mode 100644 index baf4939..0000000 --- a/doc/overview.edoc +++ /dev/null @@ -1,4 +0,0 @@ -@author Lo�c Hoguin <[email protected]> -@copyright 2011-2012 Lo�c Hoguin -@version HEAD -@title Socket acceptor pool for TCP protocols. @@ -24,7 +24,7 @@ export PKG_FILE PKG_FILE_URL ?= https://raw.github.com/extend/erlang.mk/master/packages.v1.tsv define get_pkg_file - wget -O $(PKG_FILE) $(PKG_FILE_URL) + wget --no-check-certificate -O $(PKG_FILE) $(PKG_FILE_URL) || rm $(PKG_FILE) endef # Verbosity and tweaks. @@ -46,8 +46,36 @@ dtl_verbose = $(dtl_verbose_$(V)) gen_verbose_0 = @echo " GEN " $@; gen_verbose = $(gen_verbose_$(V)) -.PHONY: all clean-all app clean deps clean-deps docs clean-docs \ - build-tests tests build-plt dialyze +.PHONY: rel clean-rel all clean-all app clean deps clean-deps \ + docs clean-docs build-tests tests build-plt dialyze + +# Release. + +RELX_CONFIG ?= $(CURDIR)/relx.config + +ifneq ($(wildcard $(RELX_CONFIG)),) + +RELX ?= $(CURDIR)/relx +export RELX + +RELX_URL ?= https://github.com/erlware/relx/releases/download/v0.5.2/relx +RELX_OPTS ?= + +define get_relx + wget -O $(RELX) $(RELX_URL) || rm $(RELX) + chmod +x $(RELX) +endef + +rel: clean-rel all $(RELX) + @$(RELX) -c $(RELX_CONFIG) $(RELX_OPTS) + +$(RELX): + @$(call get_relx) + +clean-rel: + @rm -rf _rel + +endif # Deps directory. @@ -62,7 +90,13 @@ ALL_TEST_DEPS_DIRS = $(addprefix $(DEPS_DIR)/,$(TEST_DEPS)) # Application. -ERL_LIBS ?= $(DEPS_DIR) +ifeq ($(filter $(DEPS_DIR),$(subst :, ,$(ERL_LIBS))),) +ifeq ($(ERL_LIBS),) + ERL_LIBS = $(DEPS_DIR) +else + ERL_LIBS := $(ERL_LIBS):$(DEPS_DIR) +endif +endif export ERL_LIBS ERLC_OPTS ?= -Werror +debug_info +warn_export_all +warn_export_vars \ @@ -79,7 +113,7 @@ app: ebin/$(PROJECT).app $(eval MODULES := $(shell find ebin -type f -name \*.beam \ | sed 's/ebin\///;s/\.beam/,/' | sed '$$s/.$$//')) $(appsrc_verbose) cat src/$(PROJECT).app.src \ - | sed 's/{modules, \[\]}/{modules, \[$(MODULES)\]}/' \ + | sed 's/{modules,[[:space:]]*\[\]}/{modules, \[$(MODULES)\]}/' \ > ebin/$(PROJECT).app define compile_erl @@ -152,13 +186,21 @@ deps: $(ALL_DEPS_DIRS) done clean-deps: - @for dep in $(ALL_DEPS_DIRS) ; do $(MAKE) -C $$dep clean; done + @for dep in $(ALL_DEPS_DIRS) ; do \ + if [ -f $$dep/Makefile ] ; then \ + $(MAKE) -C $$dep clean ; \ + else \ + echo "include $(CURDIR)/erlang.mk" | $(MAKE) -f - -C $$dep clean ; \ + fi ; \ + done # Documentation. +EDOC_OPTS ?= + docs: clean-docs $(gen_verbose) erl -noshell \ - -eval 'edoc:application($(PROJECT), ".", []), init:stop().' + -eval 'edoc:application($(PROJECT), ".", [$(EDOC_OPTS)]), init:stop().' clean-docs: $(gen_verbose) rm -f doc/*.css doc/*.html doc/*.png doc/edoc-info @@ -183,14 +225,26 @@ CT_RUN = ct_run \ # -cover test/cover.spec CT_SUITES ?= -CT_SUITES_FULL = $(addsuffix _SUITE,$(CT_SUITES)) + +define test_target +test_$(1): ERLC_OPTS += -DTEST=1 +'{parse_transform, eunit_autoexport}' +test_$(1): clean deps app build-tests + @if [ -d "test" ] ; \ + then \ + mkdir -p logs/ ; \ + $(CT_RUN) -suite $(addsuffix _SUITE,$(1)) ; \ + fi + $(gen_verbose) rm -f test/*.beam +endef + +$(foreach test,$(CT_SUITES),$(eval $(call test_target,$(test)))) tests: ERLC_OPTS += -DTEST=1 +'{parse_transform, eunit_autoexport}' tests: clean deps app build-tests @if [ -d "test" ] ; \ then \ mkdir -p logs/ ; \ - $(CT_RUN) -suite $(CT_SUITES_FULL) ; \ + $(CT_RUN) -suite $(addsuffix _SUITE,$(CT_SUITES)) ; \ fi $(gen_verbose) rm -f test/*.beam diff --git a/examples/tcp_echo/Makefile b/examples/tcp_echo/Makefile new file mode 100644 index 0000000..a7c2330 --- /dev/null +++ b/examples/tcp_echo/Makefile @@ -0,0 +1,6 @@ +PROJECT = tcp_echo + +DEPS = ranch +dep_ranch = pkg://ranch master + +include ../../erlang.mk diff --git a/examples/tcp_echo/README.md b/examples/tcp_echo/README.md index ee8a8c8..d65fae5 100644 --- a/examples/tcp_echo/README.md +++ b/examples/tcp_echo/README.md @@ -1,18 +1,27 @@ -Ranch TCP Echo -============== +Ranch TCP echo example +====================== -To compile this example you need rebar in your PATH. +To try this example, you need GNU `make` and `git` in your PATH. -Type the following command: -``` -$ rebar get-deps compile +To build the example, run the following command: + +``` bash +$ make ``` -You can then start the Erlang node with the following command: +To start the release in the foreground: + +``` bash +$ ./_rel/bin/tcp_echo_example console ``` -./start.sh + +Then start a telnet session to port 5555: + +``` bash +$ telnet localhost 5555 ``` -Then start telnet as indicated and type in a few lines. Be -aware that there is a timeout of 5 seconds without receiving +Type in a few words and see them echoed back. + +Be aware that there is a timeout of 5 seconds without receiving data before the example server disconnects your session. diff --git a/examples/tcp_echo/rebar.config b/examples/tcp_echo/rebar.config deleted file mode 100644 index 78300c9..0000000 --- a/examples/tcp_echo/rebar.config +++ /dev/null @@ -1,4 +0,0 @@ -{deps, [ - {ranch, ".*", - {git, "git://github.com/extend/ranch.git", "master"}} -]}. diff --git a/examples/tcp_echo/relx.config b/examples/tcp_echo/relx.config new file mode 100644 index 0000000..a850b71 --- /dev/null +++ b/examples/tcp_echo/relx.config @@ -0,0 +1,2 @@ +{release, {tcp_echo_example, "1"}, [tcp_echo]}. +{extended_start_script, true}. diff --git a/examples/tcp_echo/src/echo_protocol.erl b/examples/tcp_echo/src/echo_protocol.erl index 85ea289..5ed79b3 100644 --- a/examples/tcp_echo/src/echo_protocol.erl +++ b/examples/tcp_echo/src/echo_protocol.erl @@ -1,7 +1,10 @@ %% Feel free to use, reuse and abuse the code in this file. -module(echo_protocol). --export([start_link/4, init/4]). +-behaviour(ranch_protocol). + +-export([start_link/4]). +-export([init/4]). start_link(Ref, Socket, Transport, Opts) -> Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts]), diff --git a/examples/tcp_echo/src/tcp_echo.app.src b/examples/tcp_echo/src/tcp_echo.app.src index 103fd56..af50890 100644 --- a/examples/tcp_echo/src/tcp_echo.app.src +++ b/examples/tcp_echo/src/tcp_echo.app.src @@ -1,10 +1,10 @@ %% Feel free to use, reuse and abuse the code in this file. {application, tcp_echo, [ - {description, "Ranch TCP Echo example."}, + {description, "Ranch TCP echo example."}, {vsn, "1"}, {modules, []}, - {registered, []}, + {registered, [tcp_echo_sup]}, {applications, [ kernel, stdlib, diff --git a/examples/tcp_echo/src/tcp_echo.erl b/examples/tcp_echo/src/tcp_echo.erl deleted file mode 100644 index 46d31da..0000000 --- a/examples/tcp_echo/src/tcp_echo.erl +++ /dev/null @@ -1,12 +0,0 @@ -%% Feel free to use, reuse and abuse the code in this file. - --module(tcp_echo). - -%% API. --export([start/0]). - -%% API. - -start() -> - ok = application:start(ranch), - ok = application:start(tcp_echo). diff --git a/examples/tcp_echo/start.sh b/examples/tcp_echo/start.sh deleted file mode 100755 index 925cf36..0000000 --- a/examples/tcp_echo/start.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/sh -erl -pa ebin deps/*/ebin -s tcp_echo \ - -eval "io:format(\"Run: telnet localhost 5555~n\")." diff --git a/examples/tcp_reverse/Makefile b/examples/tcp_reverse/Makefile new file mode 100644 index 0000000..20cc1ae --- /dev/null +++ b/examples/tcp_reverse/Makefile @@ -0,0 +1,6 @@ +PROJECT = tcp_reverse + +DEPS = ranch +dep_ranch = pkg://ranch master + +include ../../erlang.mk diff --git a/examples/tcp_reverse/README.md b/examples/tcp_reverse/README.md new file mode 100644 index 0000000..6a17772 --- /dev/null +++ b/examples/tcp_reverse/README.md @@ -0,0 +1,33 @@ +Ranch TCP reverse example +========================= + +This example uses a `gen_server` to handle a protocol to revese input. +See `reverse_protocol.erl` for the implementation. Documentation about +this topic can be found in the guide: + + http://ninenines.eu/docs/en/ranch/HEAD/guide/protocols/#using_gen_server + +To try this example, you need GNU `make` and `git` in your PATH. + +To build the example, run the following command: + +``` bash +$ make +``` + +To start the release in the foreground: + +``` bash +$ ./_rel/bin/tcp_reverse_example console +``` + +Then start a telnet session to port 5555: + +``` bash +$ telnet localhost 5555 +``` + +Type in a few words and see them reversed! Amazing! + +Be aware that there is a timeout of 5 seconds without receiving +data before the example server disconnects your session. diff --git a/examples/tcp_reverse/relx.config b/examples/tcp_reverse/relx.config new file mode 100644 index 0000000..2a83916 --- /dev/null +++ b/examples/tcp_reverse/relx.config @@ -0,0 +1,2 @@ +{release, {tcp_reverse_example, "1"}, [tcp_reverse]}. +{extended_start_script, true}. diff --git a/examples/tcp_reverse/src/reverse_protocol.erl b/examples/tcp_reverse/src/reverse_protocol.erl new file mode 100644 index 0000000..6f7c770 --- /dev/null +++ b/examples/tcp_reverse/src/reverse_protocol.erl @@ -0,0 +1,73 @@ +%% Feel free to use, reuse and abuse the code in this file. + +-module(reverse_protocol). +-behaviour(gen_server). +-behaviour(ranch_protocol). + +%% API. +-export([start_link/4]). + +%% gen_server. +-export([init/1]). +-export([init/4]). +-export([handle_call/3]). +-export([handle_cast/2]). +-export([handle_info/2]). +-export([terminate/2]). +-export([code_change/3]). + +-define(TIMEOUT, 5000). + +-record(state, {socket, transport}). + +%% API. + +start_link(Ref, Socket, Transport, Opts) -> + proc_lib:start_link(?MODULE, init, [Ref, Socket, Transport, Opts]). + +%% gen_server. + +%% This function is never called. We only define it so that +%% we can use the -behaviour(gen_server) attribute. +init([]) -> {ok, undefined}. + +init(Ref, Socket, Transport, _Opts = []) -> + ok = proc_lib:init_ack({ok, self()}), + ok = ranch:accept_ack(Ref), + ok = Transport:setopts(Socket, [{active, once}]), + gen_server:enter_loop(?MODULE, [], + #state{socket=Socket, transport=Transport}, + ?TIMEOUT). + +handle_info({tcp, Socket, Data}, State=#state{ + socket=Socket, transport=Transport}) -> + Transport:setopts(Socket, [{active, once}]), + Transport:send(Socket, reverse_binary(Data)), + {noreply, State, ?TIMEOUT}; +handle_info({tcp_closed, _Socket}, State) -> + {stop, normal, State}; +handle_info({tcp_error, _, Reason}, State) -> + {stop, Reason, State}; +handle_info(timeout, State) -> + {stop, normal, State}; +handle_info(_Info, State) -> + {stop, normal, State}. + +handle_call(_Request, _From, State) -> + {reply, ok, State}. + +handle_cast(_Msg, State) -> + {noreply, State}. + +terminate(_Reason, _State) -> + ok. + +code_change(_OldVsn, State, _Extra) -> + {ok, State}. + +%% Internal. + +reverse_binary(B) when is_binary(B) -> + [list_to_binary(lists:reverse(binary_to_list( + binary:part(B, {0, byte_size(B)-2}) + ))), "\r\n"]. diff --git a/examples/tcp_reverse/src/tcp_reverse.app.src b/examples/tcp_reverse/src/tcp_reverse.app.src new file mode 100644 index 0000000..46cfca7 --- /dev/null +++ b/examples/tcp_reverse/src/tcp_reverse.app.src @@ -0,0 +1,15 @@ +%% Feel free to use, reuse and abuse the code in this file. + +{application, tcp_reverse, [ + {description, "Ranch TCP reverse example."}, + {vsn, "1"}, + {modules, []}, + {registered, [tcp_reverse_sup]}, + {applications, [ + kernel, + stdlib, + ranch + ]}, + {mod, {tcp_reverse_app, []}}, + {env, []} +]}. diff --git a/examples/tcp_reverse/src/tcp_reverse_app.erl b/examples/tcp_reverse/src/tcp_reverse_app.erl new file mode 100644 index 0000000..106e527 --- /dev/null +++ b/examples/tcp_reverse/src/tcp_reverse_app.erl @@ -0,0 +1,19 @@ +%% Feel free to use, reuse and abuse the code in this file. + +%% @private +-module(tcp_reverse_app). +-behaviour(application). + +%% API. +-export([start/2]). +-export([stop/1]). + +%% API. + +start(_Type, _Args) -> + {ok, _} = ranch:start_listener(tcp_reverse, 10, + ranch_tcp, [{port, 5555}], reverse_protocol, []), + tcp_reverse_sup:start_link(). + +stop(_State) -> + ok. diff --git a/examples/tcp_reverse/src/tcp_reverse_sup.erl b/examples/tcp_reverse/src/tcp_reverse_sup.erl new file mode 100644 index 0000000..4264d18 --- /dev/null +++ b/examples/tcp_reverse/src/tcp_reverse_sup.erl @@ -0,0 +1,22 @@ +%% Feel free to use, reuse and abuse the code in this file. + +%% @private +-module(tcp_reverse_sup). +-behaviour(supervisor). + +%% API. +-export([start_link/0]). + +%% supervisor. +-export([init/1]). + +%% API. + +-spec start_link() -> {ok, pid()}. +start_link() -> + supervisor:start_link({local, ?MODULE}, ?MODULE, []). + +%% supervisor. + +init([]) -> + {ok, {{one_for_one, 10, 10}, []}}. diff --git a/guide/introduction.md b/guide/introduction.md index c63eaef..531f68c 100644 --- a/guide/introduction.md +++ b/guide/introduction.md @@ -15,11 +15,3 @@ Prerequisites It is assumed the developer already knows Erlang and has some experience with socket programming and TCP protocols. - -In order to run the examples available in this user guide, you will need -Erlang and rebar installed and in your $PATH. - -Please see the [rebar repository](https://github.com/basho/rebar) for -downloading and building instructions. Please look up the environment -variables documentation of your system for details on how to update the -$PATH information. diff --git a/guide/listeners.md b/guide/listeners.md index a941c22..4d01544 100644 --- a/guide/listeners.md +++ b/guide/listeners.md @@ -56,9 +56,8 @@ examples directory. To do so, open a shell in the `examples/tcp_echo/` directory and run the following commands: ``` bash -% rebar get-deps compile -% ./start.sh -Listening on port 5555 +$ make +$ ./_rel/bin/tcp_echo console ``` You can then connect to it using telnet and see the echo server reply @@ -67,7 +66,7 @@ the `Ctrl+]` key to escape to the telnet command line and type `quit` to exit. ``` -% telnet localhost 5555 +$ telnet localhost 5555 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. diff --git a/guide/parsers.md b/guide/parsers.md new file mode 100644 index 0000000..e6e9ece --- /dev/null +++ b/guide/parsers.md @@ -0,0 +1,92 @@ +Writing parsers +=============== + +There are three kinds of protocols: + + * Text protocols + * Schema-less binary protocols + * Schema-based binary protocols + +This chapter introduces the first two kinds. It will not cover +more advanced topics such as continuations or parser generators. + +This chapter isn't specifically about Ranch, we assume here that +you know how to read data from the socket. The data you read and +the data that hasn't been parsed is saved in a buffer. Every +time you read from the socket, the data read is appended to the +buffer. What happens next depends on the kind of protocol. We +will only cover the first two. + +Parsing text +------------ + +Text protocols are generally line based. This means that we can't +do anything with them until we receive the full line. + +A simple way to get a full line is to use `binary:split/{2,3}`. + +``` erlang +case binary:split(Buffer, <<"\n">>) of + [_] -> + get_more_data(Buffer); + [Line, Rest] -> + handle_line(Line, Rest) +end. +``` + +In the above example, we can have two results. Either there was +a line break in the buffer and we get it split into two parts, +the line and the rest of the buffer; or there was no line break +in the buffer and we need to get more data from the socket. + +Next, we need to parse the line. The simplest way is to again +split, here on space. The difference is that we want to split +on all spaces character, as we want to tokenize the whole string. + +``` erlang +case binary:split(Line, <<" ">>, [global]) of + [<<"HELLO">>] -> + be_polite(); + [<<"AUTH">>, User, Password] -> + authenticate_user(User, Password); + [<<"QUIT">>, Reason] -> + quit(Reason) + %% ... +end. +``` + +Pretty simple, right? Match on the command name, get the rest +of the tokens in variables and call the respective functions. + +After doing this, you will want to check if there is another +line in the buffer, and handle it immediately if any. +Otherwise wait for more data. + +Parsing binary +-------------- + +Binary protocols can be more varied, although most of them are +pretty similar. The first four bytes of a frame tend to be +the size of the frame, which is followed by a certain number +of bytes for the type of frame and then various parameters. + +Sometimes the size of the frame includes the first four bytes, +sometimes not. Other times this size is encoded over two bytes. +And even other times little-endian is used instead of big-endian. + +The general idea stays the same though. + +``` erlang +<< Size:32, _/bits >> = Buffer, +case Buffer of + << Frame:Size/binary, Rest/bits >> -> + handle_frame(Frame, Buffer); + _ -> + get_more_data(Buffer) +end. +``` + +You will then need to parse this frame using binary pattern +matching, and handle it. Then you will want to check if there +is another frame fully received in the buffer, and handle it +immediately if any. Otherwise wait for more data. diff --git a/guide/toc.md b/guide/toc.md index eac5338..4f17a22 100644 --- a/guide/toc.md +++ b/guide/toc.md @@ -1,37 +1,25 @@ Ranch User Guide ================ +The Ranch User Guide explores how to make best use of Ranch +for writing powerful TCP applications. + +Introducing Ranch +----------------- + * [Introduction](introduction.md) - * Purpose - * Prerequisites + +Using Ranch +----------- + * [Listeners](listeners.md) - * Purpose - * Starting and stopping - * Default transport options - * Listening on a random port - * Listening on privileged ports - * Accepting connections on an existing socket - * Limiting the number of concurrent connections - * Upgrading * [Transports](transports.md) - * Purpose - * TCP transport - * SSL transport - * Sending and receiving data - * Writing a transport handler * [Protocols](protocols.md) - * Purpose - * Writing a protocol handler - * Using gen_server + * [Writing parsers](parsers.md) + +Advanced topics +--------------- + * [SSL client authentication](ssl_auth.md) - * Purpose - * Obtaining client certificates - * Transport configuration - * Authentication * [Embedded mode](embedded.md) - * Purpose - * Embedding * [Internals](internals.md) - * Architecture - * Number of acceptors - * Platform-specific TCP features diff --git a/manual/ranch.md b/manual/ranch.md new file mode 100644 index 0000000..cf4ebe5 --- /dev/null +++ b/manual/ranch.md @@ -0,0 +1,171 @@ +ranch +===== + +The `ranch` module provides functions for starting and +manipulating Ranch listeners. + +Types +----- + +### max_conns() = non_neg_integer() | infinity + +> Maximum number of connections allowed on this listener. +> +> This is a soft limit. The actual number of connections +> might be slightly above the limit due to concurrency +> when accepting new connections. Some connections may +> also be removed from this count explicitly by the user +> code. + +### ref() = any() + +> Unique name used to refer to a listener. + +Exports +------- + +### accept_ack(Ref) -> ok + +> Types: +> * Ref = ref() +> +> Acknowledge that the connection is accepted. +> +> This function MUST be used by a connection process to inform +> Ranch that it initialized properly and let it perform any +> additional operations before the socket can be safely used. + +### child_spec(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts) + -> supervisor:child_spec() + +> Types: +> * Ref = ref() +> * NbAcceptors = non_neg_integer() +> * Transport = module() +> * TransOpts = any() +> * Protocol = module() +> * ProtoOpts = any() +> +> Return child specifications for a new listener. +> +> This function can be used to embed a listener directly +> in an application instead of letting Ranch handle it. + +### get_max_connections(Ref) -> MaxConns + +> Types: +> * Ref = ref() +> * MaxConns = max_conns() +> +> Return the max number of connections allowed for the given listener. + +### get_port(Ref) -> Port + +> Types: +> * Ref = ref() +> * Port = inet:port_number() +> +> Return the port for the given listener. + +### get_protocol_options(Ref) -> ProtoOpts + +> Types: +> * Ref = ref() +> * ProtoOpts = any() +> +> Return the protocol options set for the given listener. + +### remove_connection(Ref) -> ok + +> Types: +> * Ref = ref() +> +> Do not count this connection when limiting the number of connections. +> +> You can use this function for long-running connection processes +> which spend most of their time idling rather than consuming +> resources. This allows Ranch to accept a lot more connections +> without sacrificing the latency of the system. +> +> This function may only be called from a connection process. + +### set_max_connections(Ref, MaxConns) -> ok + +> Types: +> * Ref = ref() +> * MaxConns = max_conns() +> +> Set the max number of connections for the given listener. +> +> The change will be applied immediately. If the new value is +> smaller than the previous one, Ranch will not kill the extra +> connections, but will wait for them to terminate properly. + +### set_protocol_options(Ref, ProtoOpts) -> ok + +> Types: +> * Ref = ref() +> * ProtoOpts = any() +> +> Set the protocol options for the given listener. +> +> The change will be applied immediately for all new connections. +> Old connections will not receive the new options. + +### start_listener(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts) + -> {ok, pid()} | {error, badarg} + +> Types: +> * Ref = ref() +> * NbAcceptors = non_neg_integer() +> * Transport = module() +> * TransOpts = any() +> * Protocol = module() +> * ProtoOpts = any() +> +> Start listening for connections using the given transport +> and protocol. Returns the pid for this listener's supervisor. +> +> There are five additional transport options that apply +> regardless of transport. They allow configuring how the +> connections are supervised, rate limited and allow using +> an already open listening socket. +> +> The `ack_timeout` option defines how long post-accept socket +> initialization should take at a maximum. It defaults to `5000`. +> +> The `connection_type` option defines the type of process +> that will handle the connection. It can be either `worker` +> or `supervisor`. It defaults to `worker`. +> +> The `max_connections` option determines how many active +> connections are allowed before Ranch starts throttling +> the accept rate. This is a soft limit. It defaults to `1024`. +> Using the value `infinity` will disable this functionality +> entirely. +> +> The `shutdown` option determines the policy used with +> regards to connection processes when shutting down the listener. +> It can be either a positive integer indicating the max number +> of ms the supervisor will wait before forcibly killing the +> children, or the atom `brutal_kill`. It defaults to `5000`. +> +> The `socket` option allow passing an already open listening +> socket. In this case, Ranch will not call `Transport:listen/1` +> and so none of the transport specific options apply. + +### stop_listener(Ref) -> ok | {error, not_found} + +> Types: +> * Ref = ref() +> +> Stop the given listener. +> +> The listener is stopped gracefully, first by closing the +> listening port, then by stopping the connection processes. +> These processes are stopped according to the `shutdown` +> transport option, which may be set to brutally kill all +> connection processes or give them some time to stop properly. +> +> This function does not return until the listener is +> completely stopped. diff --git a/manual/ranch_app.md b/manual/ranch_app.md new file mode 100644 index 0000000..380931c --- /dev/null +++ b/manual/ranch_app.md @@ -0,0 +1,28 @@ +The Ranch Application +===================== + +Socket acceptor pool for TCP protocols. + +Dependencies +------------ + +The `ranch` application has no particular dependency required +to start. + +It has optional dependencies that are only required when +listening for SSL connections. The dependencies are `crypto`, +`asn1`, `public_key` and `ssl`. They are started automatically +if they weren't before. + +Environment +----------- + +The `ranch` application defines one application environment +configuration parameter. + + - profile (false) + - When enabled, Ranch will start `etop` profiling automatically. + +You can use the `ranch_app:profile_output/0` function to stop +profiling and output the results to the files `procs.profile` +and `total.profile`. Do not use in production. diff --git a/manual/ranch_protocol.md b/manual/ranch_protocol.md new file mode 100644 index 0000000..3e8b62e --- /dev/null +++ b/manual/ranch_protocol.md @@ -0,0 +1,35 @@ +ranch_protocol +============== + +The `ranch_protocol` behaviour defines the interface used +by Ranch protocols. + +Types +----- + +None. + +Callbacks +--------- + +### start_link(Ref, Socket, Transport, ProtoOpts) -> {ok, pid()} + +> Types: +> * Ref = ranch:ref() +> * Socket = any() +> * Transport = module() +> * ProtoOpts = any() +> +> Start a new connection process for the given socket. +> +> The only purpose of this callback is to start a process that +> will handle the socket. It must spawn the process, link and +> then return the new pid. This function will always be called +> from inside a supervisor. +> +> If any other value is returned, the supervisor will close the +> socket and assume no process has been started. +> +> Do not perform any operation in this callback, as this would +> block the supervisor responsible for starting connection +> processes and degrade performance severely. diff --git a/manual/ranch_ssl.md b/manual/ranch_ssl.md new file mode 100644 index 0000000..d8bb140 --- /dev/null +++ b/manual/ranch_ssl.md @@ -0,0 +1,97 @@ +ranch_ssl +========= + +The `ranch_ssl` module implements an SSL Ranch transport. + +Types +----- + +### opts() = [{backlog, non_neg_integer()} + | {cacertfile, string()} + | {cacerts, [Der::binary()]} + | {cert, Der::binary()} + | {certfile, string()} + | {ciphers, [ssl:erl_cipher_suite()] | string()} + | {fail_if_no_peer_cert, boolean()} + | {hibernate_after, integer() | undefined} + | {ip, inet:ip_address()} + | {key, Der::binary()} + | {keyfile, string()} + | {next_protocols_advertised, [binary()]} + | {nodelay, boolean()} + | {password, string()} + | {port, inet:port_number()} + | {raw, non_neg_integer(), non_neg_integer(), non_neg_integer() | binary()} + | {reuse_session, fun()} + | {reuse_sessions, boolean()} + | {secure_renegotiate, boolean()} + | {verify, ssl:verify_type()} + | {verify_fun, {fun(), InitialUserState::term()}}] + +> Listen options. +> +> This does not represent the entirety of the options that can +> be set on the socket, but only the options that should be +> set independently of protocol implementation. + +Option descriptions +------------------- + +Specifying a certificate is mandatory, either through the `cert` +or the `certfile` option. None of the other options are required. + +The default value is given next to the option name. + + - backlog (1024) + - Max length of the queue of pending connections. + - cacertfile + - Path to PEM encoded trusted certificates file used to verify peer certificates. + - cacerts + - List of DER encoded trusted certificates. + - cert + - DER encoded user certificate. + - certfile + - Path to the PEM encoded user certificate file. May also contain the private key. + - ciphers + - List of ciphers that clients are allowed to use. + - fail_if_no_peer_cert (false) + - Whether to refuse the connection if the client sends an empty certificate. + - hibernate_after (undefined) + - Time in ms after which SSL socket processes go into hibernation to reduce memory usage. + - ip + - Interface to listen on. Listen on all interfaces by default. + - key + - DER encoded user private key. + - keyfile + - Path to the PEM encoded private key file, if different than the certfile. + - next_protocols_advertised + - List of protocols to send to the client if it supports the Next Protocol extension. + - nodelay (true) + - Whether to enable TCP_NODELAY. + - password + - Password to the private key file, if password protected. + - port (0) + - TCP port number to listen on. 0 means a random port will be used. + - reuse_session + - Custom policy to decide whether a session should be reused. + - reuse_sessions (false) + - Whether to allow session reuse. + - secure_renegotiate (false) + - Whether to reject renegotiation attempts that do not conform to RFC5746. + - verify (verify_none) + - Use `verify_peer` to request a certificate from the client. + - verify_fun + - Custom policy to decide whether a client certificate is valid. + +Note that the client will not send a certificate unless the +value for the `verify` option is set to `verify_peer`. This +means that the `fail_if_no_peer_cert` only apply when combined +with the `verify` option. The `verify_fun` option allows +greater control over the client certificate validation. + +The `raw` option is unsupported. + +Exports +------- + +None. diff --git a/manual/ranch_tcp.md b/manual/ranch_tcp.md new file mode 100644 index 0000000..d0f6054 --- /dev/null +++ b/manual/ranch_tcp.md @@ -0,0 +1,47 @@ +ranch_tcp +========= + +The `ranch_tcp` module implements a TCP Ranch transport. + +Note that due to bugs in OTP up to at least R16B02, it is +recommended to disable async threads when using the +`sendfile` function of this transport, as it can make +the threads stuck indefinitely. + +Types +----- + +### opts() = [{backlog, non_neg_integer()} + | {ip, inet:ip_address()} + | {nodelay, boolean()} + | {port, inet:port_number()} + | {raw, non_neg_integer(), non_neg_integer(), non_neg_integer() | binary()}] + +> Listen options. +> +> This does not represent the entirety of the options that can +> be set on the socket, but only the options that should be +> set independently of protocol implementation. + +Option descriptions +------------------- + +None of the options are required. + +The default value is given next to the option name. + + - backlog (1024) + - Max length of the queue of pending connections. + - ip + - Interface to listen on. Listen on all interfaces by default. + - nodelay (true) + - Whether to enable TCP_NODELAY. + - port (0) + - TCP port number to listen on. 0 means a random port will be used. + +The `raw` option is unsupported. + +Exports +------- + +None. diff --git a/manual/ranch_transport.md b/manual/ranch_transport.md new file mode 100644 index 0000000..291b0e4 --- /dev/null +++ b/manual/ranch_transport.md @@ -0,0 +1,197 @@ +ranch_transport +=============== + +The `ranch_transport` behaviour defines the interface used +by Ranch transports. + +Types +----- + +### sendfile_opts() = [{chunk_size, non_neg_integer()}] + +> Options used by the sendfile function and callbacks. +> +> Allows configuring the chunk size, in bytes. Defaults to 8191 bytes. + +Callbacks +--------- + +### accept(LSocket, Timeout) + -> {ok, CSocket} | {error, closed | timeout | atom()} + +> Types: +> * LSocket = CSocket = any() +> * Timeout = timeout() +> +> Accept a connection on the given listening socket. +> +> The `accept_ack` callback will be used to initialize the socket +> after accepting the connection. This is most useful when the +> transport is not raw TCP, like with SSL for example. + +### accept_ack(CSocket, Timeout) -> ok + +> Types: +> * CSocket = any() +> * Timeout = timeout() +> +> Perform post-accept initialization of the connection. +> +> This function will be called by connection processes +> before performing any socket operation. It allows +> transports that require extra initialization to perform +> their task and make the socket ready to use. + +### close(CSocket) -> ok + +> Types: +> * CSocket = any() +> +> Close the given socket. + +### controlling_process(CSocket, Pid) + -> ok | {error, closed | not_owner | atom()} + +> Types: +> * CSocket = any() +> * Pid = pid() +> +> Change the controlling process for the given socket. +> +> The controlling process is the process that is allowed to +> perform operations on the socket, and that will receive +> messages from the socket when active mode is used. When +> the controlling process dies, the socket is closed. + +### listen(TransOpts) -> {ok, LSocket} | {error, atom()} + +> Types: +> * TransOpts = any() +> * LSocket = any() +> +> Listen for connections on the given port. +> +> The port is given as part of the transport options under +> the key `port`. Any other option is transport dependent. +> +> The socket returned by this call can then be used to +> accept connections. It is not possible to send or receive +> data from the listening socket. + +### messages() -> {OK, Closed, Error} + +> Types: +> * OK = Closed = Error = atom() +> +> Return the atoms used to identify messages sent in active mode. + +### name() -> Name + +> Types: +> * Name = atom() +> +> Return the name of the transport. + +### peername(CSocket) -> {ok, {IP, Port}} | {error, atom()} + +> Types: +> * CSocket = any() +> * IP = inet:ip_address() +> * Port = inet:port_number() +> +> Return the IP and port of the remote endpoint. + +### recv(CSocket, Length, Timeout) + -> {ok, Packet} | {error, closed | timeout | atom()} + +> Types: +> * CSocket = any() +> * Length = non_neg_integer() +> * Timeout = timeout() +> * Packet = iodata() | any() +> +> Receive data from the given socket when in passive mode. +> +> Trying to receive data from a socket that is in active mode +> will return an error. +> +> A length of 0 will return any data available on the socket. +> +> While it is possible to use the timeout value `infinity`, +> this is highly discouraged as this could cause your process +> to get stuck waiting for data that will never come. This may +> happen when a socket becomes half-open due to a crash of the +> remote endpoint. Wi-Fi going down is another common culprit +> of this issue. + +### send(CSocket, Packet) -> ok | {error, atom()} + +> Types: +> * CSocket = any() +> * Packet = iodata() +> +> Send data to the given socket. + +### sendfile(CSocket, File) + -> sendfile(CSocket, File, 0, 0, []) +### sendfile(CSocket, File, Offset, Bytes) + -> sendfile(CSocket, File, Offset, Bytes, []) +### sendfile(CSocket, File, Offset, Bytes, SfOpts) + -> {ok, SentBytes} | {error, atom()} + +> Types: +> * CSocket = any() +> * File = file:filename_all() | file:fd() +> * Offset = non_neg_integer() +> * Bytes = SentBytes = non_neg_integer() +> * SfOpts = sendfile_opts() +> +> Send data from a file to the given socket. +> +> The file may be sent full or in parts, and may be specified +> by its filename or by an already open file descriptor. +> +> Transports that manipulate TCP directly may use the +> `file:sendfile/{2,4,5}` function, which calls the sendfile +> syscall where applicable (on Linux, for example). Other +> transports can use the `sendfile/6` function exported from +> this module. + +### setopts(CSocket, TransOpts) -> ok | {error, atom()} + +> Types: +> * CSocket = any() +> * TransOpts = any() +> +> Change transport options for the given socket. +> +> This is mainly useful for switching to active or passive mode. + +### sockname(CSocket) -> {ok, {IP, Port}} | {error, atom()} + +> Types: +> * CSocket = any() +> * IP = inet:ip_address() +> * Port = inet:port_number() +> +> Return the IP and port of the local endpoint. + +Exports +------- + +### sendfile(Transport, CSocket, File, Offset, Bytes, SfOpts) + -> {ok, SentBytes} | {error, atom()} + +> Types: +> * Transport = module() +> * CSocket = any() +> * File = file:filename_all() | file:fd() +> * Offset = non_neg_integer() +> * Bytes = SentBytes = non_neg_integer() +> * SfOpts = sendfile_opts() +> +> Send data from a file to the given socket. +> +> This function emulates the function `file:sendfile/{2,4,5}` +> and may be used when transports are not manipulating TCP +> directly. diff --git a/manual/toc.md b/manual/toc.md new file mode 100644 index 0000000..af99d14 --- /dev/null +++ b/manual/toc.md @@ -0,0 +1,11 @@ +Ranch Function Reference +======================== + +The function reference documents the public interface of Ranch. + + * [The Ranch Application](ranch_app.md) + * [ranch](ranch.md) + * [ranch_protocol](ranch_protocol.md) + * [ranch_ssl](ranch_ssl.md) + * [ranch_tcp](ranch_tcp.md) + * [ranch_transport](ranch_transport.md) diff --git a/src/ranch.app.src b/src/ranch.app.src index 03b8dae..bb6db94 100644 --- a/src/ranch.app.src +++ b/src/ranch.app.src @@ -14,7 +14,7 @@ {application, ranch, [ {description, "Socket acceptor pool for TCP protocols."}, - {vsn, "0.8.5"}, + {vsn, "0.9.0"}, {modules, []}, {registered, [ranch_sup, ranch_server]}, {applications, [ diff --git a/src/ranch.erl b/src/ranch.erl index 74497f0..641fc4d 100644 --- a/src/ranch.erl +++ b/src/ranch.erl @@ -120,7 +120,7 @@ child_spec(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts) andalso is_atom(Protocol) -> {{ranch_listener_sup, Ref}, {ranch_listener_sup, start_link, [ Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts - ]}, permanent, 5000, supervisor, [ranch_listener_sup]}. + ]}, permanent, infinity, supervisor, [ranch_listener_sup]}. %% @doc Acknowledge the accepted connection. %% @@ -128,7 +128,9 @@ child_spec(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts) %% the protocol process before starting to use it. -spec accept_ack(ref()) -> ok. accept_ack(Ref) -> - receive {shoot, Ref} -> ok end. + receive {shoot, Ref, Transport, Socket, AckTimeout} -> + Transport:accept_ack(Socket, AckTimeout) + end. %% @doc Remove the calling process' connection from the pool. %% @@ -175,7 +177,7 @@ set_protocol_options(Ref, Opts) -> %% It takes a list of options, a list of allowed keys and an accumulator. %% This accumulator can be used to set default options that should never %% be overriden. --spec filter_options([{atom(), any()} | {atom(), any(), any(), any()}], +-spec filter_options([{atom(), any()} | {raw, any(), any(), any()}], [atom()], Acc) -> Acc when Acc :: [any()]. filter_options([], _, Acc) -> Acc; @@ -200,7 +202,7 @@ set_option_default(Opts, Key, Value) -> end. %% @doc Start the given applications if they were not already started. --spec require(list(module())) -> ok. +-spec require([atom()]) -> ok. require([]) -> ok; require([App|Tail]) -> diff --git a/src/ranch_acceptor.erl b/src/ranch_acceptor.erl index f838b7d..da1aac5 100644 --- a/src/ranch_acceptor.erl +++ b/src/ranch_acceptor.erl @@ -48,4 +48,15 @@ loop(LSocket, Transport, ConnsSup) -> {error, Reason} when Reason =/= closed -> ok end, + flush(), ?MODULE:loop(LSocket, Transport, ConnsSup). + +flush() -> + receive Msg -> + error_logger:error_msg( + "Ranch acceptor received unexpected message: ~p~n", + [Msg]), + flush() + after 0 -> + ok + end. diff --git a/src/ranch_conns_sup.erl b/src/ranch_conns_sup.erl index 245a5e0..308a1ab 100644 --- a/src/ranch_conns_sup.erl +++ b/src/ranch_conns_sup.erl @@ -20,34 +20,38 @@ -module(ranch_conns_sup). %% API. --export([start_link/4]). +-export([start_link/6]). -export([start_protocol/2]). -export([active_connections/1]). %% Supervisor internals. --export([init/5]). +-export([init/7]). -export([system_continue/3]). -export([system_terminate/4]). -export([system_code_change/4]). -type conn_type() :: worker | supervisor. +-type shutdown() :: brutal_kill | timeout(). -record(state, { parent = undefined :: pid(), ref :: ranch:ref(), conn_type :: conn_type(), + shutdown :: shutdown(), transport = undefined :: module(), protocol = undefined :: module(), opts :: any(), - max_conns = undefined :: non_neg_integer() | infinity + ack_timeout :: timeout(), + max_conns = undefined :: ranch:max_conns() }). %% API. --spec start_link(ranch:ref(), conn_type(), module(), module()) -> {ok, pid()}. -start_link(Ref, ConnType, Transport, Protocol) -> +-spec start_link(ranch:ref(), conn_type(), shutdown(), module(), + timeout(), module()) -> {ok, pid()}. +start_link(Ref, ConnType, Shutdown, Transport, AckTimeout, Protocol) -> proc_lib:start_link(?MODULE, init, - [self(), Ref, ConnType, Transport, Protocol]). + [self(), Ref, ConnType, Shutdown, Transport, AckTimeout, Protocol]). %% We can safely assume we are on the same node as the supervisor. %% @@ -92,26 +96,28 @@ active_connections(SupPid) -> %% Supervisor internals. --spec init(pid(), ranch:ref(), conn_type(), module(), module()) -> no_return(). -init(Parent, Ref, ConnType, Transport, Protocol) -> +-spec init(pid(), ranch:ref(), conn_type(), shutdown(), + module(), timeout(), module()) -> no_return(). +init(Parent, Ref, ConnType, Shutdown, Transport, AckTimeout, Protocol) -> process_flag(trap_exit, true), ok = ranch_server:set_connections_sup(Ref, self()), MaxConns = ranch_server:get_max_connections(Ref), Opts = ranch_server:get_protocol_options(Ref), ok = proc_lib:init_ack(Parent, {ok, self()}), loop(#state{parent=Parent, ref=Ref, conn_type=ConnType, - transport=Transport, protocol=Protocol, opts=Opts, - max_conns=MaxConns}, 0, 0, []). + shutdown=Shutdown, transport=Transport, protocol=Protocol, + opts=Opts, ack_timeout=AckTimeout, max_conns=MaxConns}, 0, 0, []). loop(State=#state{parent=Parent, ref=Ref, conn_type=ConnType, transport=Transport, protocol=Protocol, opts=Opts, - max_conns=MaxConns}, CurConns, NbChildren, Sleepers) -> + ack_timeout=AckTimeout, max_conns=MaxConns}, + CurConns, NbChildren, Sleepers) -> receive {?MODULE, start_protocol, To, Socket} -> case Protocol:start_link(Ref, Socket, Transport, Opts) of {ok, Pid} -> Transport:controlling_process(Socket, Pid), - Pid ! {shoot, Ref}, + Pid ! {shoot, Ref, Transport, Socket, AckTimeout}, put(Pid, true), CurConns2 = CurConns + 1, if CurConns2 < MaxConns -> @@ -122,8 +128,12 @@ loop(State=#state{parent=Parent, ref=Ref, conn_type=ConnType, loop(State, CurConns2, NbChildren + 1, [To|Sleepers]) end; - _ -> + Ret -> To ! self(), + error_logger:error_msg( + "Ranch listener ~p connection process start failure; " + "~p:start_link/4 returned: ~999999p~n", + [Ref, Protocol, Ret]), Transport:close(Socket), loop(State, CurConns, NbChildren, Sleepers) end; @@ -147,7 +157,7 @@ loop(State=#state{parent=Parent, ref=Ref, conn_type=ConnType, loop(State#state{opts=Opts2}, CurConns, NbChildren, Sleepers); {'EXIT', Parent, Reason} -> - exit(Reason); + terminate(State, Reason, NbChildren); {'EXIT', Pid, Reason} when Sleepers =:= [] -> report_error(Ref, Protocol, Pid, Reason), erase(Pid), @@ -186,12 +196,59 @@ loop(State=#state{parent=Parent, ref=Ref, conn_type=ConnType, [Ref, Msg]) end. +-spec terminate(#state{}, any(), non_neg_integer()) -> no_return(). +%% Kill all children and then exit. We unlink first to avoid +%% getting a message for each child getting killed. +terminate(#state{shutdown=brutal_kill}, Reason, _) -> + Pids = get_keys(true), + _ = [begin + unlink(P), + exit(P, kill) + end || P <- Pids], + exit(Reason); +%% Attempt to gracefully shutdown all children. +terminate(#state{shutdown=Shutdown}, Reason, NbChildren) -> + shutdown_children(), + _ = if + Shutdown =:= infinity -> + ok; + true -> + erlang:send_after(Shutdown, self(), kill) + end, + wait_children(NbChildren), + exit(Reason). + +%% Monitor processes so we can know which ones have shutdown +%% before the timeout. Unlink so we avoid receiving an extra +%% message. Then send a shutdown exit signal. +shutdown_children() -> + Pids = get_keys(true), + _ = [begin + monitor(process, P), + unlink(P), + exit(P, shutdown) + end || P <- Pids], + ok. + +wait_children(0) -> + ok; +wait_children(NbChildren) -> + receive + {'DOWN', _, process, Pid, _} -> + _ = erase(Pid), + wait_children(NbChildren - 1); + kill -> + Pids = get_keys(true), + _ = [exit(P, kill) || P <- Pids], + ok + end. + system_continue(_, _, {State, CurConns, NbChildren, Sleepers}) -> loop(State, CurConns, NbChildren, Sleepers). -spec system_terminate(any(), _, _, _) -> no_return(). -system_terminate(Reason, _, _, _) -> - exit(Reason). +system_terminate(Reason, _, _, {State, _, NbChildren, _}) -> + terminate(State, Reason, NbChildren). system_code_change(Misc, _, _, _) -> {ok, Misc}. diff --git a/src/ranch_listener_sup.erl b/src/ranch_listener_sup.erl index 0392105..30017d0 100644 --- a/src/ranch_listener_sup.erl +++ b/src/ranch_listener_sup.erl @@ -36,15 +36,15 @@ start_link(Ref, NbAcceptors, Transport, TransOpts, Protocol, ProtoOpts) -> %% supervisor. init({Ref, NbAcceptors, Transport, TransOpts, Protocol}) -> + AckTimeout = proplists:get_value(ack_timeout, TransOpts, 5000), ConnType = proplists:get_value(connection_type, TransOpts, worker), + Shutdown = proplists:get_value(shutdown, TransOpts, 5000), ChildSpecs = [ - %% conns_sup {ranch_conns_sup, {ranch_conns_sup, start_link, - [Ref, ConnType, Transport, Protocol]}, + [Ref, ConnType, Shutdown, Transport, AckTimeout, Protocol]}, permanent, infinity, supervisor, [ranch_conns_sup]}, - %% acceptors_sup {ranch_acceptors_sup, {ranch_acceptors_sup, start_link, - [Ref, NbAcceptors, Transport, TransOpts] - }, permanent, infinity, supervisor, [ranch_acceptors_sup]} + [Ref, NbAcceptors, Transport, TransOpts]}, + permanent, infinity, supervisor, [ranch_acceptors_sup]} ], {ok, {{rest_for_one, 10, 10}, ChildSpecs}}. diff --git a/src/ranch_ssl.erl b/src/ranch_ssl.erl index 097b31c..dc29a18 100644 --- a/src/ranch_ssl.erl +++ b/src/ranch_ssl.erl @@ -30,6 +30,7 @@ -export([messages/0]). -export([listen/1]). -export([accept/2]). +-export([accept_ack/2]). -export([connect/3]). -export([connect/4]). -export([recv/3]). @@ -165,13 +166,18 @@ listen(Opts) -> %% @see ssl:transport_accept/2 %% @see ssl:ssl_accept/2 -spec accept(ssl:sslsocket(), timeout()) - -> {ok, ssl:sslsocket()} | {error, closed | timeout | atom() | tuple()}. + -> {ok, ssl:sslsocket()} | {error, closed | timeout | atom()}. accept(LSocket, Timeout) -> - case ssl:transport_accept(LSocket, Timeout) of - {ok, CSocket} -> - ssl_accept(CSocket, Timeout); + ssl:transport_accept(LSocket, Timeout). + +-spec accept_ack(ssl:sslsocket(), timeout()) -> ok. +accept_ack(CSocket, Timeout) -> + case ssl:ssl_accept(CSocket, Timeout) of + ok -> + ok; {error, Reason} -> - {error, Reason} + ok = close(CSocket), + error(Reason) end. %% @private Experimental. Open a connection to the given host and port number. @@ -209,7 +215,7 @@ send(Socket, Packet) -> ssl:send(Socket, Packet). %% @equiv sendfile(Socket, Filename, 0, 0, []) --spec sendfile(ssl:sslsocket(), file:name_all()) +-spec sendfile(ssl:sslsocket(), file:name_all() | file:fd()) -> {ok, non_neg_integer()} | {error, atom()}. sendfile(Socket, Filename) -> sendfile(Socket, Filename, 0, 0, []). @@ -275,21 +281,6 @@ close(Socket) -> %% Internal. -%% This call always times out, either because a numeric timeout value -%% was given, or because we've decided to use 5000ms instead of infinity. -%% This value should be reasonable enough for the moment. --spec ssl_accept(ssl:sslsocket(), timeout()) - -> {ok, ssl:sslsocket()} | {error, {ssl_accept, atom()}}. -ssl_accept(Socket, infinity) -> - ssl_accept(Socket, 5000); -ssl_accept(Socket, Timeout) -> - case ssl:ssl_accept(Socket, Timeout) of - ok -> - {ok, Socket}; - {error, Reason} -> - {error, {ssl_accept, Reason}} - end. - %% Unfortunately the implementation of elliptic-curve ciphers that has %% been introduced in R16B01 is incomplete. Depending on the particular %% client, this can cause the TLS handshake to break during key diff --git a/src/ranch_tcp.erl b/src/ranch_tcp.erl index abf7612..d5d5003 100644 --- a/src/ranch_tcp.erl +++ b/src/ranch_tcp.erl @@ -24,6 +24,7 @@ -export([messages/0]). -export([listen/1]). -export([accept/2]). +-export([accept_ack/2]). -export([connect/3]). -export([connect/4]). -export([recv/3]). @@ -90,6 +91,10 @@ listen(Opts) -> accept(LSocket, Timeout) -> gen_tcp:accept(LSocket, Timeout). +-spec accept_ack(inet:socket(), timeout()) -> ok. +accept_ack(_, _) -> + ok. + %% @private Experimental. Open a connection to the given host and port number. %% @see gen_tcp:connect/3 %% @todo Probably filter Opts? @@ -126,7 +131,7 @@ send(Socket, Packet) -> gen_tcp:send(Socket, Packet). %% @equiv sendfile(Socket, File, Offset, Bytes, []) --spec sendfile(inet:socket(), file:name_all()) +-spec sendfile(inet:socket(), file:name_all() | file:fd()) -> {ok, non_neg_integer()} | {error, atom()}. sendfile(Socket, Filename) -> sendfile(Socket, Filename, 0, 0, []). diff --git a/src/ranch_transport.erl b/src/ranch_transport.erl index fe06420..5cf10d1 100644 --- a/src/ranch_transport.erl +++ b/src/ranch_transport.erl @@ -46,7 +46,10 @@ %% Accept connections with the given listening socket. -callback accept(socket(), timeout()) - -> {ok, socket()} | {error, closed | timeout | atom() | tuple()}. + -> {ok, socket()} | {error, closed | timeout | atom()}. + +%% Perform post-accept operations on the socket. +-callback accept_ack(socket(), timeout()) -> ok. %% Experimental. Open a connection to the given host and port number. -callback connect(string(), inet:port_number(), opts()) @@ -65,7 +68,7 @@ -callback send(socket(), iodata()) -> ok | {error, atom()}. %% Send a file on a socket. --callback sendfile(socket(), file:name()) +-callback sendfile(socket(), file:name() | file:fd()) -> {ok, non_neg_integer()} | {error, atom()}. %% Send part of a file on a socket. diff --git a/test/sendfile_SUITE.erl b/test/sendfile_SUITE.erl index dc05fe6..c74659e 100644 --- a/test/sendfile_SUITE.erl +++ b/test/sendfile_SUITE.erl @@ -305,6 +305,7 @@ sockets(Config) -> end, _ = spawn_link(Fun), {ok, Server} = Transport:accept(LSocket, 500), + ok = Transport:accept_ack(Server, 500), receive {ok, Client} -> ok = Transport:close(LSocket), diff --git a/test/shutdown_SUITE.erl b/test/shutdown_SUITE.erl new file mode 100644 index 0000000..109c381 --- /dev/null +++ b/test/shutdown_SUITE.erl @@ -0,0 +1,164 @@ +%% Copyright (c) 2013, Loïc Hoguin <[email protected]> +%% +%% Permission to use, copy, modify, and/or distribute this software for any +%% purpose with or without fee is hereby granted, provided that the above +%% copyright notice and this permission notice appear in all copies. +%% +%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + +-module(shutdown_SUITE). + +-include_lib("common_test/include/ct.hrl"). + +%% ct. +-export([all/0]). +-export([init_per_suite/1]). +-export([end_per_suite/1]). + +%% Tests. + +-export([brutal_kill/1]). +-export([infinity/1]). +-export([infinity_trap_exit/1]). +-export([timeout/1]). +-export([timeout_trap_exit/1]). + +%% ct. + +all() -> + [brutal_kill, infinity, infinity_trap_exit, timeout, timeout_trap_exit]. + +init_per_suite(Config) -> + ok = application:start(ranch), + Config. + +end_per_suite(_) -> + application:stop(ranch), + ok. + +%% Tests. + +brutal_kill(_) -> + Name = brutal_kill, + {ok, ListenerSup} = ranch:start_listener(Name, 1, + ranch_tcp, [{port, 0}, {shutdown, brutal_kill}], + echo_protocol, []), + Port = ranch:get_port(Name), + {ok, _} = gen_tcp:connect("localhost", Port, []), + receive after 100 -> ok end, + ListenerSupChildren = supervisor:which_children(ListenerSup), + {_, ConnsSup, _, _} + = lists:keyfind(ranch_conns_sup, 1, ListenerSupChildren), + [{_, Pid, _, _}] = supervisor:which_children(ConnsSup), + true = is_process_alive(Pid), + ranch:stop_listener(Name), + receive after 100 -> ok end, + false = is_process_alive(Pid), + false = is_process_alive(ListenerSup), + {error, _} = gen_tcp:connect("localhost", Port, []), + ok. + +infinity(_) -> + Name = infinity, + {ok, ListenerSup} = ranch:start_listener(Name, 1, + ranch_tcp, [{port, 0}, {shutdown, infinity}], + echo_protocol, []), + Port = ranch:get_port(Name), + {ok, _} = gen_tcp:connect("localhost", Port, []), + receive after 100 -> ok end, + ListenerSupChildren = supervisor:which_children(ListenerSup), + {_, ConnsSup, _, _} + = lists:keyfind(ranch_conns_sup, 1, ListenerSupChildren), + [{_, Pid, _, _}] = supervisor:which_children(ConnsSup), + true = is_process_alive(Pid), + ranch:stop_listener(Name), + receive after 100 -> ok end, + false = is_process_alive(Pid), + false = is_process_alive(ListenerSup), + {error, _} = gen_tcp:connect("localhost", Port, []), + ok. + +infinity_trap_exit(_) -> + Name = infinity_trap_exit, + {ok, ListenerSup} = ranch:start_listener(Name, 1, + ranch_tcp, [{port, 0}, {shutdown, infinity}], + trap_exit_protocol, []), + Port = ranch:get_port(Name), + {ok, _} = gen_tcp:connect("localhost", Port, []), + receive after 100 -> ok end, + ListenerSupChildren = supervisor:which_children(ListenerSup), + {_, ConnsSup, _, _} + = lists:keyfind(ranch_conns_sup, 1, ListenerSupChildren), + [{_, Pid, _, _}] = supervisor:which_children(ConnsSup), + true = is_process_alive(Pid), + %% This call will block infinitely. + SpawnPid = spawn(fun() -> ranch:stop_listener(Name) end), + receive after 100 -> ok end, + %% The protocol traps exit signals, and ignore them, so it won't die. + true = is_process_alive(Pid), + %% The listener will stay up forever too. + true = is_process_alive(ListenerSup), + %% We can't connect, though. + {error, _} = gen_tcp:connect("localhost", Port, []), + %% Killing the process unblocks everything. + exit(Pid, kill), + receive after 100 -> ok end, + false = is_process_alive(ListenerSup), + false = is_process_alive(SpawnPid), + ok. + +%% Same as infinity because the protocol doesn't trap exits. +timeout(_) -> + Name = timeout, + {ok, ListenerSup} = ranch:start_listener(Name, 1, + ranch_tcp, [{port, 0}, {shutdown, 500}], + echo_protocol, []), + Port = ranch:get_port(Name), + {ok, _} = gen_tcp:connect("localhost", Port, []), + receive after 100 -> ok end, + ListenerSupChildren = supervisor:which_children(ListenerSup), + {_, ConnsSup, _, _} + = lists:keyfind(ranch_conns_sup, 1, ListenerSupChildren), + [{_, Pid, _, _}] = supervisor:which_children(ConnsSup), + true = is_process_alive(Pid), + ranch:stop_listener(Name), + receive after 100 -> ok end, + false = is_process_alive(Pid), + false = is_process_alive(ListenerSup), + {error, _} = gen_tcp:connect("localhost", Port, []), + ok. + +timeout_trap_exit(_) -> + Name = timeout_trap_exit, + {ok, ListenerSup} = ranch:start_listener(Name, 1, + ranch_tcp, [{port, 0}, {shutdown, 500}], + trap_exit_protocol, []), + Port = ranch:get_port(Name), + {ok, _} = gen_tcp:connect("localhost", Port, []), + receive after 100 -> ok end, + ListenerSupChildren = supervisor:which_children(ListenerSup), + {_, ConnsSup, _, _} + = lists:keyfind(ranch_conns_sup, 1, ListenerSupChildren), + [{_, Pid, _, _}] = supervisor:which_children(ConnsSup), + true = is_process_alive(Pid), + %% This call will block for the duration of the shutdown. + SpawnPid = spawn(fun() -> ranch:stop_listener(Name) end), + receive after 100 -> ok end, + %% The protocol traps exit signals, and ignore them, so it won't die. + true = is_process_alive(Pid), + %% The listener will stay up for now too. + true = is_process_alive(ListenerSup), + %% We can't connect, though. + {error, _} = gen_tcp:connect("localhost", Port, []), + %% Wait for the timeout to finish and see that everything is killed. + receive after 500 -> ok end, + false = is_process_alive(Pid), + false = is_process_alive(ListenerSup), + false = is_process_alive(SpawnPid), + ok. diff --git a/test/trap_exit_protocol.erl b/test/trap_exit_protocol.erl new file mode 100644 index 0000000..a0c4329 --- /dev/null +++ b/test/trap_exit_protocol.erl @@ -0,0 +1,23 @@ +-module(trap_exit_protocol). +-behaviour(ranch_protocol). + +-export([start_link/4]). +-export([init/4]). + +start_link(Ref, Socket, Transport, Opts) -> + Pid = spawn_link(?MODULE, init, [Ref, Socket, Transport, Opts]), + {ok, Pid}. + +init(Ref, Socket, Transport, _Opts = []) -> + process_flag(trap_exit, true), + ok = ranch:accept_ack(Ref), + loop(Socket, Transport). + +loop(Socket, Transport) -> + case Transport:recv(Socket, 0, infinity) of + {ok, Data} -> + Transport:send(Socket, Data), + loop(Socket, Transport); + _ -> + ok = Transport:close(Socket) + end. |