summary refs log tree commit diff stats
diff options
context:
space:
mode:
authordef <dennis@felsin9.de>2015-03-02 03:08:17 +0100
committerdef <dennis@felsin9.de>2015-03-17 19:39:02 +0100
commit477b3594ebdeec0d8ecdf1f050a61af7e0f96cbf (patch)
tree0c61ec0f372382d99ec042487007ee8656d8829e
parent07a50caf64d1ed2891349cff9a22b53c4ef61c2d (diff)
downloadNim-477b3594ebdeec0d8ecdf1f050a61af7e0f96cbf.tar.gz
Speed up asynchttpserver significantly using all the previous changes
- Export socket field of AsyncHttpServer and addHeaders proc for templates

- Make respond a template instead of proc because of how often it's called.
  This means no more "await" when invoking it.

- Optimize respond template with special case for empty headers and
  Content-Length entry

- newRequest doesn't allocate a hostname and body anymore because they're
  copied in later

- Major changes to processClient to prevent allocations and copies
-rw-r--r--lib/pure/asynchttpserver.nim98
-rw-r--r--lib/pure/asyncnet.nim2
2 files changed, 53 insertions, 47 deletions
diff --git a/lib/pure/asynchttpserver.nim b/lib/pure/asynchttpserver.nim
index 64242234c..c288f6089 100644
--- a/lib/pure/asynchttpserver.nim
+++ b/lib/pure/asynchttpserver.nim
@@ -21,7 +21,7 @@
 ##
 ##    var server = newAsyncHttpServer()
 ##    proc cb(req: Request) {.async.} =
-##      await req.respond(Http200, "Hello World")
+##      req.respond(Http200, "Hello World")
 ##
 ##    asyncCheck server.serve(Port(8080), cb)
 ##    runForever()
@@ -38,7 +38,7 @@ type
     body*: string
 
   AsyncHttpServer* = ref object
-    socket: AsyncSocket
+    socket*: AsyncSocket
     reuseAddr: bool
 
   HttpCode* = enum
@@ -99,7 +99,7 @@ proc newAsyncHttpServer*(reuseAddr = true): AsyncHttpServer =
   new result
   result.reuseAddr = reuseAddr
 
-proc addHeaders(msg: var string, headers: StringTableRef) =
+proc addHeaders*(msg: var string, headers: StringTableRef) =
   for k, v in headers:
     msg.add(k & ": " & v & "\c\L")
 
@@ -109,22 +109,22 @@ proc sendHeaders*(req: Request, headers: StringTableRef): Future[void] =
   addHeaders(msg, headers)
   return req.client.send(msg)
 
-proc respond*(req: Request, code: HttpCode,
-        content: string, headers = newStringTable()) {.async.} =
+template respond*(req: Request, code: HttpCode,
+                  content: string, headers: StringTableRef = nil) =
   ## Responds to the request with the specified ``HttpCode``, headers and
   ## content.
   ##
-  ## This procedure will **not** close the client socket.
-  var customHeaders = headers
-  customHeaders["Content-Length"] = $content.len
+  ## This template will **not** close the client socket.
   var msg = "HTTP/1.1 " & $code & "\c\L"
-  msg.addHeaders(customHeaders)
-  await req.client.send(msg & "\c\L" & content)
+
+  if headers != nil:
+    msg.addHeaders(headers)
+  msg.add("Content-Length: " & $content.len & "\c\L\c\L")
+  msg.add(content)
+  result = req.client.send(msg)
 
 proc newRequest(): Request =
   result.headers = newStringTable(modeCaseInsensitive)
-  result.hostname = ""
-  result.body = ""
 
 proc parseHeader(line: string): tuple[key, value: string] =
   var i = 0
@@ -149,77 +149,83 @@ proc sendStatus(client: AsyncSocket, status: string): Future[void] =
 proc processClient(client: AsyncSocket, address: string,
                    callback: proc (request: Request):
                       Future[void] {.closure, gcsafe.}) {.async.} =
+  var request: Request
+  request.url = initUri()
+  request.headers = newStringTable(modeCaseInsensitive)
+  var line = newStringOfCap(80)
+  var key, value = ""
+
   while not client.isClosed:
     # GET /path HTTP/1.1
     # Header: val
     # \n
-    var request = newRequest()
-    request.hostname = address
+    request.headers.resetStringTable(modeCaseInsensitive)
+    request.hostname.shallowCopy(address)
     assert client != nil
     request.client = client
 
     # First line - GET /path HTTP/1.1
-    let line = await client.recvLine() # TODO: Timeouts.
+    line.setLen(0)
+    client.recvLineInto(line) # TODO: Timeouts.
     if line == "":
       client.close()
       return
-    let lineParts = line.split(' ')
-    if lineParts.len != 3:
-      await request.respond(Http400, "Invalid request. Got: " & line)
-      continue
 
-    let reqMethod = lineParts[0]
-    let path = lineParts[1]
-    let protocol = lineParts[2]
+    var i = 0
+    for linePart in line.split(' '):
+      case i
+      of 0: request.reqMethod.shallowCopy(linePart.normalize)
+      of 1: parseUri(linePart, request.url)
+      of 2:
+        try:
+          request.protocol = parseProtocol(linePart)
+        except ValueError:
+          request.respond(Http400, "Invalid request protocol. Got: " &
+              linePart)
+          continue
+      else:
+        request.respond(Http400, "Invalid request. Got: " & line)
+        continue
+      inc i
 
     # Headers
-    var i = 0
     while true:
       i = 0
-      let headerLine = await client.recvLine()
-      if headerLine == "":
-        client.close(); return
-      if headerLine == "\c\L": break
-      # TODO: Compiler crash
-      #let (key, value) = parseHeader(headerLine)
-      let kv = parseHeader(headerLine)
-      request.headers[kv.key] = kv.value
+      line.setLen(0)
+      client.recvLineInto(line)
 
-    request.reqMethod = reqMethod
-    request.url = parseUri(path)
-    try:
-      request.protocol = protocol.parseProtocol()
-    except ValueError:
-      asyncCheck request.respond(Http400, "Invalid request protocol. Got: " &
-          protocol)
-      continue
+      if line == "":
+        client.close(); return
+      if line == "\c\L": break
+      let (key, value) = parseHeader(line)
+      request.headers[key] = value
 
-    if reqMethod.normalize == "post":
+    if request.reqMethod == "post":
       # Check for Expect header
       if request.headers.hasKey("Expect"):
         if request.headers["Expect"].toLower == "100-continue":
           await client.sendStatus("100 Continue")
         else:
           await client.sendStatus("417 Expectation Failed")
-    
+
       # Read the body
       # - Check for Content-length header
       if request.headers.hasKey("Content-Length"):
         var contentLength = 0
         if parseInt(request.headers["Content-Length"], contentLength) == 0:
-          await request.respond(Http400, "Bad Request. Invalid Content-Length.")
+          request.respond(Http400, "Bad Request. Invalid Content-Length.")
         else:
           request.body = await client.recv(contentLength)
           assert request.body.len == contentLength
       else:
-        await request.respond(Http400, "Bad Request. No Content-Length.")
+        request.respond(Http400, "Bad Request. No Content-Length.")
         continue
 
-    case reqMethod.normalize
+    case request.reqMethod
     of "get", "post", "head", "put", "delete", "trace", "options", "connect", "patch":
       await callback(request)
     else:
-      await request.respond(Http400, "Invalid request method. Got: " & reqMethod)
+      request.respond(Http400, "Invalid request method. Got: " & request.reqMethod)
 
     # Persistent connections
     if (request.protocol == HttpVer11 and
@@ -268,7 +274,7 @@ when isMainModule:
       #echo(req.headers)
       let headers = {"Date": "Tue, 29 Apr 2014 23:40:08 GMT",
           "Content-type": "text/plain; charset=utf-8"}
-      await req.respond(Http200, "Hello World", headers.newStringTable())
+      req.respond(Http200, "Hello World", headers.newStringTable())
 
     asyncCheck server.serve(Port(5555), cb)
     runForever()
diff --git a/lib/pure/asyncnet.nim b/lib/pure/asyncnet.nim
index 7e1ff5db4..cb137cfe5 100644
--- a/lib/pure/asyncnet.nim
+++ b/lib/pure/asyncnet.nim
@@ -200,7 +200,7 @@ template readInto*(buf: cstring, size: int, socket: AsyncSocket,
     res = recvIntoFut.read()
   res
 
-template readIntoBuf(socket: AsyncSocket,
+template readIntoBuf*(socket: AsyncSocket,
     flags: set[SocketFlag]): int =
   var size = readInto(addr socket.buffer[0], BufferSize, socket, flags)
   socket.currPos = 0