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:
parent
07a50caf64
commit
477b3594eb
2 changed files with 53 additions and 47 deletions
|
|
@ -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,77 +149,83 @@ 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":
|
||||||
await client.sendStatus("100 Continue")
|
await client.sendStatus("100 Continue")
|
||||||
else:
|
else:
|
||||||
await client.sendStatus("417 Expectation Failed")
|
await client.sendStatus("417 Expectation Failed")
|
||||||
|
|
||||||
# Read the body
|
# Read the body
|
||||||
# - Check for Content-length header
|
# - Check for Content-length header
|
||||||
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()
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue