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
This commit is contained in:
def 2015-03-02 03:08:17 +01:00
commit 477b3594eb
2 changed files with 53 additions and 47 deletions

View file

@ -21,7 +21,7 @@
## ##
## var server = newAsyncHttpServer() ## var server = newAsyncHttpServer()
## proc cb(req: Request) {.async.} = ## proc cb(req: Request) {.async.} =
## await req.respond(Http200, "Hello World") ## req.respond(Http200, "Hello World")
## ##
## asyncCheck server.serve(Port(8080), cb) ## asyncCheck server.serve(Port(8080), cb)
## runForever() ## runForever()
@ -38,7 +38,7 @@ type
body*: string body*: string
AsyncHttpServer* = ref object AsyncHttpServer* = ref object
socket: AsyncSocket socket*: AsyncSocket
reuseAddr: bool reuseAddr: bool
HttpCode* = enum HttpCode* = enum
@ -99,7 +99,7 @@ proc newAsyncHttpServer*(reuseAddr = true): AsyncHttpServer =
new result new result
result.reuseAddr = reuseAddr result.reuseAddr = reuseAddr
proc addHeaders(msg: var string, headers: StringTableRef) = proc addHeaders*(msg: var string, headers: StringTableRef) =
for k, v in headers: for k, v in headers:
msg.add(k & ": " & v & "\c\L") msg.add(k & ": " & v & "\c\L")
@ -109,22 +109,22 @@ proc sendHeaders*(req: Request, headers: StringTableRef): Future[void] =
addHeaders(msg, headers) addHeaders(msg, headers)
return req.client.send(msg) return req.client.send(msg)
proc respond*(req: Request, code: HttpCode, template respond*(req: Request, code: HttpCode,
content: string, headers = newStringTable()) {.async.} = content: string, headers: StringTableRef = nil) =
## Responds to the request with the specified ``HttpCode``, headers and ## Responds to the request with the specified ``HttpCode``, headers and
## content. ## content.
## ##
## This procedure will **not** close the client socket. ## This template will **not** close the client socket.
var customHeaders = headers
customHeaders["Content-Length"] = $content.len
var msg = "HTTP/1.1 " & $code & "\c\L" 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 = proc newRequest(): Request =
result.headers = newStringTable(modeCaseInsensitive) result.headers = newStringTable(modeCaseInsensitive)
result.hostname = ""
result.body = ""
proc parseHeader(line: string): tuple[key, value: string] = proc parseHeader(line: string): tuple[key, value: string] =
var i = 0 var i = 0
@ -149,52 +149,58 @@ proc sendStatus(client: AsyncSocket, status: string): Future[void] =
proc processClient(client: AsyncSocket, address: string, proc processClient(client: AsyncSocket, address: string,
callback: proc (request: Request): callback: proc (request: Request):
Future[void] {.closure, gcsafe.}) {.async.} = 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: while not client.isClosed:
# GET /path HTTP/1.1 # GET /path HTTP/1.1
# Header: val # Header: val
# \n # \n
var request = newRequest() request.headers.resetStringTable(modeCaseInsensitive)
request.hostname = address request.hostname.shallowCopy(address)
assert client != nil assert client != nil
request.client = client request.client = client
# First line - GET /path HTTP/1.1 # First line - GET /path HTTP/1.1
let line = await client.recvLine() # TODO: Timeouts. line.setLen(0)
client.recvLineInto(line) # TODO: Timeouts.
if line == "": if line == "":
client.close() client.close()
return return
let lineParts = line.split(' ')
if lineParts.len != 3:
await request.respond(Http400, "Invalid request. Got: " & line)
continue
let reqMethod = lineParts[0] var i = 0
let path = lineParts[1] for linePart in line.split(' '):
let protocol = lineParts[2] 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 # Headers
var i = 0
while true: while true:
i = 0 i = 0
let headerLine = await client.recvLine() line.setLen(0)
if headerLine == "": client.recvLineInto(line)
if line == "":
client.close(); return client.close(); return
if headerLine == "\c\L": break if line == "\c\L": break
# TODO: Compiler crash let (key, value) = parseHeader(line)
#let (key, value) = parseHeader(headerLine) request.headers[key] = value
let kv = parseHeader(headerLine)
request.headers[kv.key] = kv.value
request.reqMethod = reqMethod if request.reqMethod == "post":
request.url = parseUri(path)
try:
request.protocol = protocol.parseProtocol()
except ValueError:
asyncCheck request.respond(Http400, "Invalid request protocol. Got: " &
protocol)
continue
if reqMethod.normalize == "post":
# Check for Expect header # Check for Expect header
if request.headers.hasKey("Expect"): if request.headers.hasKey("Expect"):
if request.headers["Expect"].toLower == "100-continue": if request.headers["Expect"].toLower == "100-continue":
@ -207,19 +213,19 @@ proc processClient(client: AsyncSocket, address: string,
if request.headers.hasKey("Content-Length"): if request.headers.hasKey("Content-Length"):
var contentLength = 0 var contentLength = 0
if parseInt(request.headers["Content-Length"], 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: else:
request.body = await client.recv(contentLength) request.body = await client.recv(contentLength)
assert request.body.len == contentLength assert request.body.len == contentLength
else: else:
await request.respond(Http400, "Bad Request. No Content-Length.") request.respond(Http400, "Bad Request. No Content-Length.")
continue continue
case reqMethod.normalize case request.reqMethod
of "get", "post", "head", "put", "delete", "trace", "options", "connect", "patch": of "get", "post", "head", "put", "delete", "trace", "options", "connect", "patch":
await callback(request) await callback(request)
else: else:
await request.respond(Http400, "Invalid request method. Got: " & reqMethod) request.respond(Http400, "Invalid request method. Got: " & request.reqMethod)
# Persistent connections # Persistent connections
if (request.protocol == HttpVer11 and if (request.protocol == HttpVer11 and
@ -268,7 +274,7 @@ when isMainModule:
#echo(req.headers) #echo(req.headers)
let headers = {"Date": "Tue, 29 Apr 2014 23:40:08 GMT", let headers = {"Date": "Tue, 29 Apr 2014 23:40:08 GMT",
"Content-type": "text/plain; charset=utf-8"} "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) asyncCheck server.serve(Port(5555), cb)
runForever() runForever()

View file

@ -200,7 +200,7 @@ template readInto*(buf: cstring, size: int, socket: AsyncSocket,
res = recvIntoFut.read() res = recvIntoFut.read()
res res
template readIntoBuf(socket: AsyncSocket, template readIntoBuf*(socket: AsyncSocket,
flags: set[SocketFlag]): int = flags: set[SocketFlag]): int =
var size = readInto(addr socket.buffer[0], BufferSize, socket, flags) var size = readInto(addr socket.buffer[0], BufferSize, socket, flags)
socket.currPos = 0 socket.currPos = 0