Skip to content

Commit

Permalink
Acceptor pool. Keep alive. Binaries for everything. Test and benchmar…
Browse files Browse the repository at this point in the history
…k script.
  • Loading branch information
knutin committed Feb 9, 2012
1 parent bf9daf6 commit 6f4eedd
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 56 deletions.
22 changes: 21 additions & 1 deletion README.md
@@ -1 +1,21 @@
My first recreational webserver.
My first recreational webserver.

Goals:

* As efficient as can be, without sacrificing
* Robustness and correctness
* Not use more processes or messages than absolutely required
* Comet/long-polling
* Well tested
* Upgrade without restart
* Traceability
* Metrics, stats, hooks
* Gzip compression for replies over a certain size

Non-goals:

* SSL
* HTTP compliance (Date headers, all verbs, pipelining, etc)
* Normal webserver features like html templating, session handling
* Virtual hosts, binding to ip addresses

3 changes: 3 additions & 0 deletions bin/ab.sh
@@ -0,0 +1,3 @@
#!/bin/bash

ab -c 15 -n 100000 -k http://localhost:8080/foobar/baz/quux
11 changes: 11 additions & 0 deletions bin/test.py
@@ -0,0 +1,11 @@
#!/usr/bin/python
import httplib2
URL = "http://localhost:8080/foo/bar/baz"

h = httplib2.Http()

for i in range(1, 3):
resp, content = h.request(URL)

print h.request(URL, headers = {"Connection": "close"})

2 changes: 2 additions & 0 deletions include/elli.hrl
@@ -0,0 +1,2 @@
-define(l2i(L), list_to_integer(L)).
-define(i2l(I), integer_to_list(I)).
Binary file added rebar
Binary file not shown.
111 changes: 56 additions & 55 deletions src/elli.erl
@@ -1,66 +1,67 @@
-module(elli).
-compile([export_all]).
-behaviour(gen_server).
-include("elli.hrl").

-define(l2i(L), list_to_integer(L)).
-define(i2l(I), integer_to_list(I)).
%% API
-export([start_link/0, start_link/1]).

start() ->
{ok, ListenSocket} = gen_tcp:listen(8080, [binary, {reuseaddr, true}, {packet, 0}]),
accept(ListenSocket).
%% gen_server callbacks
-export([init/1, handle_call/3, handle_cast/2, handle_info/2,
terminate/2, code_change/3]).

accept(ListenSocket) ->
{ok, Socket} = gen_tcp:accept(ListenSocket),
socket_loop(Socket),
exit(normal).

socket_loop(Socket) ->
%% keep alive
inet:setopts(Socket, [{packet, http}]),
request_loop(Socket).
-record(state, {socket, acceptors = 0}).

request_loop(Socket) ->
{Method, Path, Headers} = get_headers(Socket),
Body = get_body(Socket, Headers),
%%%===================================================================
%%% API
%%%===================================================================

{ok, UserHeaders, UserBody} = user_callback(Method, Path, Headers, Body),
start_link() -> start_link([]).

ResultHeaders = [{'Content-Length', size(UserBody)} | UserHeaders],
start_link(Opts) ->
gen_server:start_link(?MODULE, [Opts], []).

Response = [<<"HTTP/1.1 200 OK\r\n">>,
encode_headers(ResultHeaders), <<"\r\n">>, UserBody],
io:format("response: ~p~n", [iolist_to_binary(Response)]),
ok = gen_tcp:send(Socket, Response),

%%%===================================================================
%%% gen_server callbacks
%%%===================================================================

init([_Opts]) ->
{ok, Socket} = gen_tcp:listen(8080, [binary,
{reuseaddr, true},
{packet, raw}]),
Acceptors = [start_acceptor(Socket) || _ <- lists:seq(1, 20)],
{ok, #state{socket = Socket, acceptors = length(Acceptors)}}.

handle_call(_Req, _From, State) ->
{reply, ok, State}.

handle_cast(accepted, State) ->
start_acceptor(State#state.socket),
{noreply, State};

handle_cast(_Msg, State) ->
{noreply, State}.

%% handle_info({'EXIT', _, normal}, State) ->
%% %%start_acceptor(State#state.socket),
%% {noreply, State};

handle_info(_Info, State) ->
io:format("elli got ~p~n", [_Info]),
{noreply, State}.

terminate(_Reason, _State) ->
ok.

get_headers(Socket) ->
inet:setopts(Socket, [{active, once}]),
receive
{http, _, {http_request, Method, Path, _Version}} ->
{Method, Path, get_headers(Socket, [], 0)}
end.
get_headers(Socket, Headers, HeadersCount) ->
inet:setopts(Socket, [{active, once}]),
receive
{http, _, http_eoh} ->
Headers;
{http, _, {http_header, _, Key, _, Value}} ->
get_headers(Socket, [{Key, Value} | Headers], HeadersCount + 1)
end.

get_body(Socket, Headers) ->
ContentLength = proplists:get_value('Content-Length', Headers),
inet:setopts(Socket, [{active, false}, {packet, raw}]),
{ok, Body} = gen_tcp:recv(Socket, ?l2i(ContentLength)),
Body.


user_callback(_Method, _Path, _Headers, _Body) ->
{ok, [], <<"foobar">>}.


encode_headers([]) ->
[];
encode_headers([{K, V} | H]) ->
[atom_to_binary(K, latin1), <<": ">>, encode_value(V), <<"\r\n">>, encode_headers(H)].

encode_value(I) when is_integer(I) -> ?i2l(I).
code_change(_OldVsn, State, _Extra) ->
{ok, State}.

%%%===================================================================
%%% Internal functions
%%%===================================================================



start_acceptor(Socket) ->
elli_acceptor:start_link(self(), Socket).
27 changes: 27 additions & 0 deletions src/elli_acceptor.erl
@@ -0,0 +1,27 @@
-module(elli_acceptor).
-include("elli.hrl").

-export([start_link/2, accept/2, loop/1]).

start_link(Server, ListenSocket) ->
proc_lib:spawn_link(?MODULE, accept, [Server, ListenSocket]).


accept(Server, ListenSocket) ->
%% TODO: timeout, call ?MODULE:init again
case catch gen_tcp:accept(ListenSocket) of
{ok, Socket} ->
gen_server:cast(Server, accepted),
?MODULE:loop(Socket),
exit(normal)
end.

loop(Socket) ->
case elli_request:handle(Socket) of
keep_alive ->
loop(Socket);
close ->
gen_tcp:close(Socket),
exit(normal)
end.

71 changes: 71 additions & 0 deletions src/elli_request.erl
@@ -0,0 +1,71 @@
-module(elli_request).
-include("elli.hrl").

-export([handle/1]).

handle(Socket) ->
{Method, Path, Version, Headers} = get_headers(Socket),
Body = get_body(Socket, Headers),

{ok, UserHeaders, UserBody} = user_callback(Method, Path, Headers, Body),

ResultHeaders = [{<<"Connection">>, <<"Keep-Alive">>} | [{'Content-Length', size(UserBody)} | UserHeaders]],

Response = [<<"HTTP/1.1 200 OK\r\n">>,
encode_headers(ResultHeaders), <<"\r\n">>, UserBody],
ok = gen_tcp:send(Socket, Response),

connection_token(Version, Headers).

connection_token({1, 1}, Headers) ->
case proplists:get_value(<<"Connection">>, Headers) of
<<"close">> -> close;
_ -> keep_alive
end;

connection_token({1, 0}, Headers) ->
case proplists:get_value(<<"Connection">>, Headers) of
<<"Keep-Alive">> -> keep_alive;
_ -> close
end.

get_headers(Socket) ->
inet:setopts(Socket, [{packet, http_bin}, {active, once}]),
receive
{http, _, {http_request, Method, Path, Version}} ->
{Method, Path, Version, get_headers(Socket, [], 0)}
end.
get_headers(Socket, Headers, HeadersCount) ->
inet:setopts(Socket, [{active, once}]),
receive
{http, _, http_eoh} ->
Headers;
{http, _, {http_header, _, Key, _, Value}} ->
get_headers(Socket, [{atom_to_binary(Key, latin1), Value} | Headers],
HeadersCount + 1)
end.

get_body(Socket, Headers) ->
case proplists:get_value('Content-Length', Headers, undefined) of
undefined ->
<<>>;
ContentLength ->
inet:setopts(Socket, [{active, false}, {packet, raw}]),
{ok, Body} = gen_tcp:recv(Socket, ?l2i(ContentLength)),
Body
end.


user_callback(_Method, _Path, _Headers, _Body) ->
{ok, [], <<"foobar">>}.


encode_headers([]) ->
[];
encode_headers([{K, V} | H]) ->
[encode_value(K), <<": ">>, encode_value(V), <<"\r\n">>, encode_headers(H)].


encode_value(V) when is_integer(V) -> ?i2l(V);
encode_value(V) when is_binary(V) -> V;
encode_value(V) when is_atom(V) -> atom_to_binary(V, latin1).

0 comments on commit 6f4eedd

Please sign in to comment.