From b4985d1a6ed2887cd2669a0b9c7309fe027052ed Mon Sep 17 00:00:00 2001
From: Ryan
Date: Fri, 6 Mar 2009 19:49:52 +0100
Subject: [PATCH] working towards working keep-alive. need tests
---
Makefile | 4 +-
http_api.js | 22 +---
node.cc | 12 +-
node_http.cc | 289 +++++++++++++++++++++++++++++++-----------
spec/index.html | 137 ++++++++++++++++----
spec/specification.css | 8 +-
test/test_http_server_echo.rb | 4 +-
7 files changed, 350 insertions(+), 126 deletions(-)
diff --git a/Makefile b/Makefile
index a11ea87..d4790a4 100644
--- a/Makefile
+++ b/Makefile
@@ -1,7 +1,7 @@
EVDIR=$(HOME)/local/libev
V8INC = $(HOME)/src/v8/include
-#V8LIB = $(HOME)/src/v8/libv8_g.a
-V8LIB = $(HOME)/src/v8/libv8.a
+V8LIB = $(HOME)/src/v8/libv8_g.a
+#V8LIB = $(HOME)/src/v8/libv8.a
CFLAGS = -g -I$(V8INC) -Ideps/oi -DHAVE_GNUTLS=0 -Ideps/ebb
LDFLAGS = -lev -pthread # -lefence
diff --git a/http_api.js b/http_api.js
index 703e6aa..7eb80eb 100644
--- a/http_api.js
+++ b/http_api.js
@@ -4,24 +4,16 @@ function encode(data) {
}
var port = 8000;
-var server = new HTTP.Server("localhost", port);
+var server = new HTTPServer("localhost", port);
-server.onRequest = function (request) {
+server.onrequest = function (request) {
+ log("path: " + request.path);
+ log("query string: " + request.query_string);
- // onBody sends null on the last chunk.
- request.onBody = function (chunk) {
- if(chunk) {
- this.respond(encode(chunk));
- } else {
- this.respond(encode("\n"));
- this.respond("0\r\n\r\n");
- this.respond(null); // signals end-of-request
- }
- }
- request.respond("HTTP/1.0 200 OK\r\n");
- request.respond("Content-Type: text/plain\r\n");
- request.respond("Transfer-Encoding: chunked\r\n");
+ request.respond("HTTP/1.1 200 OK\r\n");
+ request.respond("Content-Length: 0\r\n");
request.respond("\r\n");
+ request.respond(null);
};
diff --git a/node.cc b/node.cc
index 00ccc5f..e412676 100644
--- a/node.cc
+++ b/node.cc
@@ -123,6 +123,7 @@ LogCallback (const Arguments& args)
return Undefined();
}
+
static Handle
BlockingFileReadCallback (const Arguments& args)
{
@@ -164,11 +165,14 @@ main (int argc, char *argv[])
Context::Scope context_scope(context);
Local g = Context::GetCurrent()->Global();
- g->Set( String::New("log"), FunctionTemplate::New(LogCallback)->GetFunction());
- g->Set( String::New("blockingFileRead")
- , FunctionTemplate::New(BlockingFileReadCallback)->GetFunction()
- );
+ g->Set ( String::New("log")
+ , FunctionTemplate::New(LogCallback)->GetFunction()
+ );
+
+ g->Set ( String::New("blockingFileRead")
+ , FunctionTemplate::New(BlockingFileReadCallback)->GetFunction()
+ );
Init_timer(g);
Init_tcp(g);
diff --git a/node_http.cc b/node_http.cc
index ccf615c..0c28948 100644
--- a/node_http.cc
+++ b/node_http.cc
@@ -12,6 +12,67 @@ using namespace std;
static Persistent request_template;
+static string status_lines[] =
+ { "100 Continue"
+ , "101 Switching Protocols"
+#define LEVEL_100 1
+ , "200 OK"
+ , "201 Created"
+ , "202 Accepted"
+ , "203 Non-Authoritative Information"
+ , "204 No Content"
+ , "205 Reset Content"
+ , "206 Partial Content"
+ , "207 Multi-Status"
+#define LEVEL_200 7
+ , "300 Multiple Choices"
+ , "301 Moved Permanently"
+ , "302 Moved Temporarily"
+ , "303 See Other"
+ , "304 Not Modified"
+ , "305 Use Proxy"
+ , "306 unused"
+ , "307 Temporary Redirect"
+#define LEVEL_300 7
+ , "400 Bad Request"
+ , "401 Unauthorized"
+ , "402 Payment Required"
+ , "403 Forbidden"
+ , "404 Not Found"
+ , "405 Not Allowed"
+ , "406 Not Acceptable"
+ , "407 Proxy Authentication Required"
+ , "408 Request Time-out"
+ , "409 Conflict"
+ , "410 Gone"
+ , "411 Length Required"
+ , "412 Precondition Failed"
+ , "413 Request Entity Too Large"
+ , "414 Request-URI Too Large"
+ , "415 Unsupported Media Type"
+ , "416 Requested Range Not Satisfiable"
+ , "417 Expectation Failed"
+ , "418 unused"
+ , "419 unused"
+ , "420 unused"
+ , "421 unused"
+ , "422 Unprocessable Entity"
+ , "423 Locked"
+ , "424 Failed Dependency"
+#define LEVEL_400 24
+ , "500 Internal Server Error"
+ , "501 Method Not Implemented"
+ , "502 Bad Gateway"
+ , "503 Service Temporarily Unavailable"
+ , "504 Gateway Time-out"
+ , "505 HTTP Version Not Supported"
+ , "506 Variant Also Negotiates"
+ , "507 Insufficient Storage"
+ , "508 unused"
+ , "509 unused"
+ , "510 Not Extended"
+ };
+
// globals
static Persistent path_str;
static Persistent uri_str;
@@ -60,27 +121,38 @@ private:
Persistent js_server;
};
+class HttpRequest;
+
class Connection {
public:
- Connection ()
- {
- oi_socket_init (&socket, 30.0);
- ebb_request_parser_init (&parser);
- }
- ebb_request_parser parser;
+ Connection();
+ ~Connection();
+
+ void Parse(const void *buf, size_t count);
+ void Write();
+ HttpRequest* RequestBegin ();
+ void RequestEnd (HttpRequest*);
+
oi_socket socket;
- Persistent js_onRequest;
+ Persistent js_onrequest;
+private:
+ ebb_request_parser parser;
+ list requests;
friend class Server;
};
class HttpRequest {
public:
HttpRequest (Connection &c);
+ /* Deleted from C++ as soon as possible.
+ * Javascript object might linger. This is okay
+ */
~HttpRequest();
void MakeBodyCallback (const char *base, size_t length);
Local CreateJSObject ();
+ void Respond (Handle data);
string path;
string query_string;
@@ -92,7 +164,9 @@ class HttpRequest {
Connection &connection;
ebb_request parser_info;
- private:
+
+ list output;
+ bool done;
Persistent js_object;
};
@@ -123,38 +197,54 @@ RespondCallback (const Arguments& args)
{
HandleScope scope;
- Handle field = Handle::Cast(args.Holder()->GetInternalField(0));
+ // TODO check that args.Holder()->GetInternalField(0)
+ // is not NULL if so raise INVALID_STATE_ERR
+ Handle field = Handle::Cast(args.Holder()->GetInternalField(0));
HttpRequest* request = static_cast(field->Value());
+ request->Respond(args[0]);
+}
- Handle arg = args[0];
-
- // TODO Make sure that we write reponses in the correct order. With
- // keep-alive it's possible that one response can return before the last
- // one has been sent!!!
-
- //printf("response called\n");
+void
+HttpRequest::Respond (Handle data)
+{
+ // TODO ByteArray ?
- if(arg == Null()) {
+ if(data == Null()) {
+ done = true;
+ } else {
+ Handle s = data->ToString();
+ oi_buf *buf = oi_buf_new2(s->Length());
+ s->WriteAscii(buf->base, 0, s->Length());
+ output.push_back(buf);
+ }
- //printf("response got null\n");
- delete request;
+ connection.Write();
+}
- } else {
+/*
+static Handle
+RespondHeadersCallback (const Arguments& args)
+{
+ HandleScope scope;
- Handle s = arg->ToString();
+ int status = args[0]->IntegerValue();
+ Local headers = Local::Cast(args[1]);
- //printf("response called len %d\n", s->Length());
+ for(int i = 0; i < headers->Length(); i++) {
+ Local v = headers->Get(i);
+ Local pair = Local::Cast(v);
+ if(pair->Length() != 2) {
+ assert(0); //error
+ }
- oi_buf *buf = oi_buf_new2(s->Length());
- s->WriteAscii(buf->base, 0, s->Length());
- oi_socket_write(&request->connection.socket, buf);
}
-
- return Undefined();
+
}
+*/
+
static void
on_path (ebb_request *req, const char *buf, size_t len)
@@ -224,7 +314,7 @@ on_headers_complete (ebb_request *req)
// and one argument, the request.
const int argc = 1;
Handle argv[argc] = { js_request };
- Handle r = request->connection.js_onRequest->Call(Context::GetCurrent()->Global(), argc, argv);
+ Handle r = request->connection.js_onrequest->Call(Context::GetCurrent()->Global(), argc, argv);
if(try_catch.HasCaught())
node_fatal_exception(try_catch);
@@ -252,7 +342,7 @@ static ebb_request * on_request
{
Connection *connection = static_cast (data);
- HttpRequest *request = new HttpRequest(*connection);
+ HttpRequest *request = connection->RequestBegin();
return &request->parser_info;
}
@@ -264,15 +354,8 @@ static void on_read
)
{
Connection *connection = static_cast (socket->data);
- ebb_request_parser_execute ( &connection->parser
- // FIXME change ebb to use void*
- , static_cast (buf)
- , count
- );
- if(ebb_request_parser_has_error(&connection->parser)) {
- fprintf(stderr, "parse error closing connection\n");
- oi_socket_close(&connection->socket);
- }
+ write(1, buf, count);
+ connection->Parse(buf, count);
}
static void on_close
@@ -280,27 +363,18 @@ static void on_close
)
{
Connection *connection = static_cast (socket->data);
- // TODO free requests
delete connection;
}
-static void on_drain
- ( oi_socket *socket
- )
-{
- Connection *connection = static_cast (socket->data);
- //oi_socket_close(&connection->socket);
-}
-
HttpRequest::~HttpRequest ()
{
- //printf("request is being destructed\n");
-
- connection.socket.on_drain = oi_socket_close;
+ connection.RequestEnd(this);
HandleScope scope;
- // delete a reference to the respond method
- js_object->Delete(respond_str);
+ // delete a reference c++ HttpRequest
+ js_object->SetInternalField(0, Null());
+ // dispose of Persistent handle so that
+ // it can be GC'd normally.
js_object.Dispose();
}
@@ -317,6 +391,8 @@ HttpRequest::HttpRequest (Connection &c) : connection(c)
parser_info.on_body = on_body;
parser_info.on_complete = on_request_complete;
parser_info.data = this;
+
+ done = false;
}
void
@@ -324,11 +400,11 @@ HttpRequest::MakeBodyCallback (const char *base, size_t length)
{
HandleScope handle_scope;
//
- // XXX don't always allocate onBody strings
+ // XXX don't always allocate onbody strings
//
- Handle onBody_val = js_object->Get(on_body_str);
- if (!onBody_val->IsFunction()) return;
- Handle onBody = Handle::Cast(onBody_val);
+ Handle onbody_val = js_object->Get(on_body_str);
+ if (!onbody_val->IsFunction()) return;
+ Handle onbody = Handle::Cast(onbody_val);
TryCatch try_catch;
const int argc = 1;
@@ -342,7 +418,7 @@ HttpRequest::MakeBodyCallback (const char *base, size_t length)
argv[0] = Null();
}
- Handle result = onBody->Call(js_object, argc, argv);
+ Handle result = onbody->Call(js_object, argc, argv);
if(try_catch.HasCaught())
node_fatal_exception(try_catch);
@@ -420,6 +496,7 @@ HttpRequest::CreateJSObject ()
result->Set(headers_str, headers);
js_object = Persistent::New(result);
+ // weak ref?
return scope.Close(result);
}
@@ -438,22 +515,92 @@ on_connection (oi_server *_server, struct sockaddr *addr, socklen_t len)
return NULL;
Connection *connection = new Connection();
- connection->socket.on_read = on_read;
- connection->socket.on_error = NULL;
- connection->socket.on_close = on_close;
- connection->socket.on_timeout = NULL;
- connection->socket.on_drain = on_drain;
- connection->socket.data = connection;
-
- connection->parser.new_request = on_request;
- connection->parser.data = connection;
Handle f = Handle::Cast(callback_v);
- connection->js_onRequest = Persistent::New(f);
+ connection->js_onrequest = Persistent::New(f);
return &connection->socket;
}
+Connection::Connection ()
+{
+ oi_socket_init (&socket, 30.0);
+ socket.on_read = on_read;
+ socket.on_error = NULL;
+ socket.on_close = on_close;
+ socket.on_timeout = on_close;
+ socket.on_drain = NULL;
+ socket.data = this;
+
+ ebb_request_parser_init (&parser);
+ parser.new_request = on_request;
+ parser.data = this;
+}
+
+Connection::~Connection ()
+{
+ list::iterator i = requests.begin();
+ while(i != requests.end()) {
+ delete *i; // this will call RequestEnd()
+ }
+}
+
+void
+Connection::Parse(const void *buf, size_t count)
+{
+ // FIXME change ebb_request_parser to use void* arg
+ ebb_request_parser_execute ( &parser
+ , static_cast (buf)
+ , count
+ );
+
+ if(ebb_request_parser_has_error(&parser)) {
+ fprintf(stderr, "parse error closing connection\n");
+ oi_socket_close(&socket);
+ }
+}
+
+HttpRequest *
+Connection::RequestBegin( )
+{
+ HttpRequest *request = new HttpRequest(*this);
+ requests.push_back(request);
+ return request;
+}
+
+void
+Connection::RequestEnd(HttpRequest *request)
+{
+ requests.remove(request);
+}
+
+void
+Connection::Write ( )
+{
+ if(requests.size() == 0)
+ return;
+
+ HttpRequest *request = requests.front();
+
+ while(request->output.size() > 0) {
+ oi_buf *buf = request->output.front();
+ oi_socket_write(&socket, buf);
+ request->output.pop_front();
+ }
+
+ if(request->done) {
+ if(!ebb_request_should_keep_alive(&request->parser_info)) {
+ printf("not keep-alive closing\n");
+ socket.on_drain = oi_socket_close;
+ } else {
+ printf("keep-alive\n");
+ }
+ requests.pop_front();
+ delete request;
+ Write();
+ }
+}
+
static void
server_destroy (Persistent _, void *data)
{
@@ -498,7 +645,7 @@ Server::Stop()
/* This constructor takes 2 arguments: host, port. */
static Handle
-server_constructor (const Arguments& args)
+newHTTPServer (const Arguments& args)
{
if (args.Length() < 2)
return Undefined();
@@ -544,7 +691,7 @@ Init_http (Handle target)
{
HandleScope scope;
- Local server_t = FunctionTemplate::New(server_constructor);
+ Local server_t = FunctionTemplate::New(newHTTPServer);
server_t->InstanceTemplate()->SetInternalFieldCount(1);
target->Set(String::New("HTTPServer"), server_t->GetFunction());
@@ -557,8 +704,8 @@ Init_http (Handle target)
http_version_str = Persistent::New( String::NewSymbol("http_version") );
headers_str = Persistent::New( String::NewSymbol("headers") );
- on_request_str = Persistent::New( String::NewSymbol("onRequest") );
- on_body_str = Persistent::New( String::NewSymbol("onBody") );
+ on_request_str = Persistent::New( String::NewSymbol("onrequest") );
+ on_body_str = Persistent::New( String::NewSymbol("onbody") );
respond_str = Persistent::New( String::NewSymbol("respond") );
copy_str = Persistent::New( String::New("COPY") );
diff --git a/spec/index.html b/spec/index.html
index 0b88b57..cbb0645 100644
--- a/spec/index.html
+++ b/spec/index.html
@@ -69,10 +69,9 @@
API is only a specification and does not reflect Node's
behavior—there I will try to note the difference.
- Unless otherwise noted, all functions can be considered
- non-blocking. Non-blocking means that program execution will continue
- without waiting for some I/O event (be that network or device).
-
+
Unless otherwise noted, a function is non-blocking. Non-blocking means
+ that program execution will continue without waiting for an I/O event
+ (be that network or device).
1.1 The event loop
@@ -82,9 +81,6 @@
running. If however there arn't any pending callbacks waiting for
something to happen, the program will exit.
- Only one callback is executed at a time.
-
-
1.2 Execution context
Global data is shared between callbacks.
@@ -99,10 +95,13 @@ interface HTTPServer {
readonly attribute String port ;
// networking
- attribute Function onRequest ;
+ attribute Function onrequest ;
void close(); // yet not implemented
};
+
error handling?
+
+
2.1 Request object
interface HTTPRequest {
readonly attribute String path ;
@@ -111,19 +110,85 @@ interface HTTPServer {
readonly attribute String fragment ;
readonly attribute String method ;
readonly attribute String http_version ;
+ readonly attribute Array headers ;
- readonly attribute Object headers ;
+ // ready state
+ const unsigned short HEADERS_RECEIVED = 0;
+ const unsigned short LOADING = 1;
+ const unsigned short DONE = 2;
+ readonly attribute long readyState;
- attribute Function onBody ;
+ attribute Function onbody ;
- void respond(in String data);
+ void respondHeader (in short status, in Array headers);
+ void respondBody (in ByteArray data);
};
- A request object is what is passed to HTTPServer.onRequest
.
+
issue: client ip address
+
+ A request object is what is passed to HTTPServer.onrequest
.
it represents a single HTTP request. Clients might preform HTTP
pipelining (Keep-Alive) and send multiple requests per TCP
connection—this does not affect this interface.
+
+
If any error is encountered either with the request or while using the
+ two response methods the connection to client immediately terminated.
+
+
+ respondHeader(status, headers)
+
+ This method sends the response status line and headers.
+ This method may only be called once. After the first, calling it
+ will raise an INVALID_STATE_ERR
exception.
+
+
The status
argument is an integer HTTP status response code as
+ defined in 6.1 of RFC 2616 .
+
+
The header
argument is an Array
of
+ tuples (a two-element Array
). For example
+
+
[["Content-Type", "text/plain"], ["Content-Length", 10]]
+
+ This array determines the response headers. If the
+ header
parameter includes elements that are not tuples it
+ raises SYNTAX_ERR
. If the elements of the tuples do not
+ respond to toString()
the method raises
+ SYNTAX_ERR
.
+
+
Besides the author response headers interpreters should not
+ include additional response headers. This ensures that authors
+ have a reasonably predictable API.
+
+
If the client connection was closed for any reason, calling
+ respondHeader()
will raise a NETWORK_ERR
+ exception.
+
+
+ respondBody(data)
+
+ This method must be called after respondHeader()
. If
+ respondHeader()
has not been called it will raise an
+ INVALID_STATE_ERR
exception.
+
+
When given a String
or ByteArray
the
+ interpreter will send the data.
+
+
Given a null
argument signals end-of-response.
+
+
The author must call respondBody(null)
+ for each response, even if the response has no body.
+
+ After the end-of-response, calling respondHeader()
or
+ respondBody()
will raise an INVALID_STATE_ERR
exception.
+
+
If the client connection was closed for any reason, calling
+ respondBody()
will raise a NETWORK_ERR
+ exception.
+
+
+
+
3 TCP Client
[Constructor(in String host, in String port)]
@@ -141,7 +206,7 @@ interface TCPClient {
attribute Function onopen ;
attribute Function onread ;
attribute Function onclose ;
- void write(in String data);
+ void write(in ByteArray data);
void disconnect();
};
@@ -157,11 +222,12 @@ interface TCPClient {
write(data)
Transmits data using the connection. If the connection is not yet
- established, it must raise an INVALID_STATE_ERR
exception.
-
-
write(null)
sends an EOF to the peer. Further writing
- is disabled. However the onread
callback may still
- be executed.
+ established or the connection is closed, calling write()
+ will raise an INVALID_STATE_ERR
exception.
+
+ write(null)
sends an EOF to the peer. Further writing
+ is disabled. However the onread
callback may still
+ be executed.
disconnect()
@@ -176,18 +242,17 @@ interface TCPClient {
-
The readyState
attribute
+
The readyState
attribute
represents the state of the connection. When the object is created it must
be set to CONNECTING
.
-
Once a connection is established, the readyState
-attribute's value must be changed to OPEN
, and the
-onopen
callback will be made.
+
Once a connection is established, the
+ readyState
attribute's value must be changed to
+ OPEN
, and the onopen
callback will be made.
When data is received, the onread
callback
- will be made with a single parameter: a String
containing a
- chunk of data. The user does not have the ability to control how much data
+ will be made with a single parameter: a ByteArray
containing a
+ chunk of data. The author does not have the ability to control how much data
is received nor the ability to stop the input besides disconnecting.