diff options
author | Loïc Hoguin <[email protected]> | 2011-05-05 14:03:39 +0200 |
---|---|---|
committer | Loïc Hoguin <[email protected]> | 2011-05-05 14:03:39 +0200 |
commit | 29e71cf4daec684c13047952a95ec0dc9540aad5 (patch) | |
tree | 0dfdca7886756c50cc03b25fd28fed183f409786 /src/cowboy_http_protocol.erl | |
parent | 6c1f73c53c9260d99f71676b400a27f0a853f584 (diff) | |
download | cowboy-29e71cf4daec684c13047952a95ec0dc9540aad5.tar.gz cowboy-29e71cf4daec684c13047952a95ec0dc9540aad5.tar.bz2 cowboy-29e71cf4daec684c13047952a95ec0dc9540aad5.zip |
Switch the HTTP protocol to use binary packets instead of lists.
The server now does a single recv (or more, but only if needed)
which is then sent to erlang:decode_packet/3 multiple times. Since
most requests are smaller than the default MTU on many platforms,
we benefit from this greatly.
In the case of requests with a body, the server usually read at
least part of the body on the first recv. This is bufferized
properly and used when later retrieving the body.
In the case of pipelined requests, we can end up reading many
requests in a single recv, which are then handled properly using
only the buffer containing the received data.
Diffstat (limited to 'src/cowboy_http_protocol.erl')
-rw-r--r-- | src/cowboy_http_protocol.erl | 90 |
1 files changed, 56 insertions, 34 deletions
diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl index 602ea6a..221e316 100644 --- a/src/cowboy_http_protocol.erl +++ b/src/cowboy_http_protocol.erl @@ -14,7 +14,7 @@ -module(cowboy_http_protocol). -export([start_link/3]). %% API. --export([init/3, wait_request/1]). %% FSM. +-export([init/3, parse_request/1]). %% FSM. -include("include/http.hrl"). @@ -26,7 +26,8 @@ req_empty_lines = 0 :: integer(), max_empty_lines :: integer(), timeout :: timeout(), - connection = keepalive :: keepalive | close + connection = keepalive :: keepalive | close, + buffer = <<>> :: binary() }). %% API. @@ -47,11 +48,21 @@ init(Socket, Transport, Opts) -> wait_request(#state{socket=Socket, transport=Transport, dispatch=Dispatch, max_empty_lines=MaxEmptyLines, timeout=Timeout}). +-spec parse_request(State::#state{}) -> ok. +%% @todo Use decode_packet options to limit length? +parse_request(State=#state{buffer=Buffer}) -> + case erlang:decode_packet(http_bin, Buffer, []) of + {ok, Request, Rest} -> request(Request, State#state{buffer=Rest}); + {more, _Length} -> wait_request(State); + {error, _Reason} -> error_response(400, State) + end. + -spec wait_request(State::#state{}) -> ok. -wait_request(State=#state{socket=Socket, transport=Transport, timeout=T}) -> - Transport:setopts(Socket, [{packet, http}]), +wait_request(State=#state{socket=Socket, transport=Transport, + timeout=T, buffer=Buffer}) -> case Transport:recv(Socket, 0, T) of - {ok, Request} -> request(Request, State); + {ok, Data} -> parse_request(State#state{ + buffer= << Buffer/binary, Data/binary >>}); {error, timeout} -> error_terminate(408, State); {error, closed} -> terminate(State) end. @@ -67,41 +78,50 @@ request({http_request, Method, {abs_path, AbsPath}, Version}, State=#state{socket=Socket, transport=Transport}) -> {Path, RawPath, Qs} = cowboy_dispatcher:split_path(AbsPath), ConnAtom = version_to_connection(Version), - wait_header(#http_req{socket=Socket, transport=Transport, + parse_header(#http_req{socket=Socket, transport=Transport, connection=ConnAtom, method=Method, version=Version, path=Path, raw_path=RawPath, raw_qs=Qs}, State#state{connection=ConnAtom}); request({http_request, Method, '*', Version}, State=#state{socket=Socket, transport=Transport}) -> ConnAtom = version_to_connection(Version), - wait_header(#http_req{socket=Socket, transport=Transport, + parse_header(#http_req{socket=Socket, transport=Transport, connection=ConnAtom, method=Method, version=Version, - path='*', raw_path="*", raw_qs=[]}, + path='*', raw_path= <<"*">>, raw_qs= <<>>}, State#state{connection=ConnAtom}); request({http_request, _Method, _URI, _Version}, State) -> error_terminate(501, State); -request({http_error, "\r\n"}, +request({http_error, <<"\r\n">>}, State=#state{req_empty_lines=N, max_empty_lines=N}) -> error_terminate(400, State); -request({http_error, "\r\n"}, State=#state{req_empty_lines=N}) -> - wait_request(State#state{req_empty_lines=N + 1}); +request({http_error, <<"\r\n">>}, State=#state{req_empty_lines=N}) -> + parse_request(State#state{req_empty_lines=N + 1}); request({http_error, _Any}, State) -> error_terminate(400, State). +-spec parse_header(Req::#http_req{}, State::#state{}) -> ok. +parse_header(Req, State=#state{buffer=Buffer}) -> + case erlang:decode_packet(httph_bin, Buffer, []) of + {ok, Header, Rest} -> header(Header, Req, State#state{buffer=Rest}); + {more, _Length} -> wait_header(Req, State); + {error, _Reason} -> error_response(400, State) + end. + -spec wait_header(Req::#http_req{}, State::#state{}) -> ok. wait_header(Req, State=#state{socket=Socket, - transport=Transport, timeout=T}) -> + transport=Transport, timeout=T, buffer=Buffer}) -> case Transport:recv(Socket, 0, T) of - {ok, Header} -> header(Header, Req, State); + {ok, Data} -> parse_header(Req, State#state{ + buffer= << Buffer/binary, Data/binary >>}); {error, timeout} -> error_terminate(408, State); {error, closed} -> terminate(State) end. -spec header({http_header, I::integer(), Field::http_header(), R::term(), - Value::string()} | http_eoh, Req::#http_req{}, State::#state{}) -> ok. + Value::binary()} | http_eoh, Req::#http_req{}, State::#state{}) -> ok. header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{ transport=Transport, host=undefined}, State) -> - RawHost2 = string_to_lower(RawHost), + RawHost2 = binary_to_lower(RawHost), case catch cowboy_dispatcher:split_host(RawHost2) of {Host, RawHost3, undefined} -> Port = default_port(Transport:name()), @@ -115,21 +135,21 @@ header({http_header, _I, 'Host', _R, RawHost}, Req=#http_req{ end; %% Ignore Host headers if we already have it. header({http_header, _I, 'Host', _R, _V}, Req, State) -> - wait_header(Req, State); + parse_header(Req, State); header({http_header, _I, 'Connection', _R, Connection}, Req, State) -> ConnAtom = connection_to_atom(Connection), - wait_header(Req#http_req{connection=ConnAtom, + parse_header(Req#http_req{connection=ConnAtom, headers=[{'Connection', Connection}|Req#http_req.headers]}, State#state{connection=ConnAtom}); header({http_header, _I, Field, _R, Value}, Req, State) -> - wait_header(Req#http_req{headers=[{Field, Value}|Req#http_req.headers]}, + parse_header(Req#http_req{headers=[{Field, Value}|Req#http_req.headers]}, State); %% The Host header is required. header(http_eoh, #http_req{host=undefined}, State) -> error_terminate(400, State); -header(http_eoh, Req, State) -> - handler_init(Req, State); -header({http_error, _String}, _Req, State) -> +header(http_eoh, Req, State=#state{buffer=Buffer}) -> + handler_init(Req#http_req{buffer=Buffer}, State#state{buffer= <<>>}); +header({http_error, _Bin}, _Req, State) -> error_terminate(500, State). -spec dispatch(Req::#http_req{}, State::#state{}) -> ok. @@ -139,7 +159,7 @@ dispatch(Req=#http_req{host=Host, path=Path}, %% things like url rewriting. case cowboy_dispatcher:match(Host, Path, Dispatch) of {ok, Handler, Opts, Binds} -> - wait_header(Req#http_req{bindings=Binds}, + parse_header(Req#http_req{bindings=Binds}, State#state{handler={Handler, Opts}}); {error, notfound, host} -> error_terminate(400, State); @@ -173,14 +193,17 @@ handler_loop(HandlerState, Req, State=#state{handler={Handler, _Opts}}) -> -spec handler_terminate(HandlerState::term(), Req::#http_req{}, State::#state{}) -> ok. -handler_terminate(HandlerState, Req, State=#state{handler={Handler, _Opts}}) -> +handler_terminate(HandlerState, Req=#http_req{buffer=Buffer}, + State=#state{handler={Handler, _Opts}}) -> HandlerRes = (catch Handler:terminate( Req#http_req{resp_state=locked}, HandlerState)), BodyRes = ensure_body_processed(Req), ensure_response(Req, State), case {HandlerRes, BodyRes, State#state.connection} of - {ok, ok, keepalive} -> ?MODULE:wait_request(State); - _Closed -> terminate(State) + {ok, ok, keepalive} -> + ?MODULE:parse_request(State#state{buffer=Buffer}); + _Closed -> + terminate(State) end. -spec ensure_body_processed(Req::#http_req{}) -> ok | close. @@ -226,14 +249,14 @@ terminate(#state{socket=Socket, transport=Transport}) -> version_to_connection({1, 1}) -> keepalive; version_to_connection(_Any) -> close. --spec connection_to_atom(Connection::string()) -> keepalive | close. -connection_to_atom("keep-alive") -> +-spec connection_to_atom(Connection::binary()) -> keepalive | close. +connection_to_atom(<<"keep-alive">>) -> keepalive; -connection_to_atom("close") -> +connection_to_atom(<<"close">>) -> close; connection_to_atom(Connection) -> - case string_to_lower(Connection) of - "close" -> close; + case binary_to_lower(Connection) of + <<"close">> -> close; _Any -> keepalive end. @@ -241,11 +264,10 @@ connection_to_atom(Connection) -> default_port(ssl) -> 443; default_port(_) -> 80. -%% More efficient implementation of string:to_lower. %% We are excluding a few characters on purpose. --spec string_to_lower(string()) -> string(). -string_to_lower(L) -> - [char_to_lower(C) || C <- L]. +-spec binary_to_lower(binary()) -> binary(). +binary_to_lower(L) -> + << << (char_to_lower(C)) >> || << C >> <= L >>. %% We gain noticeable speed by matching each value directly. -spec char_to_lower(char()) -> char(). |