Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Acceptor pool. Keep alive. Binaries for everything. Test and benchmar…
…k script.
- Loading branch information
Showing
8 changed files
with
191 additions
and
56 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
#!/bin/bash | ||
|
||
ab -c 15 -n 100000 -k http://localhost:8080/foobar/baz/quux |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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"}) | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
-define(l2i(L), list_to_integer(L)). | ||
-define(i2l(I), integer_to_list(I)). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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). |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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). |