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 a11ea8754e..d4790a4ee9 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 703e6aadca..7eb80eb3ee 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 00ccc5f460..e4126768f2 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 ccf615c901..0c28948b03 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 0b88b57112..cbb0645f9e 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.