Many async optimisations.
* Selectors implementation will now attempt to immediately execute an IO operation instead of waiting for a ready notification. * Removed recursion in asynchttpserver. * Improved buffered implementation of recvLine in asyncnet. * Optimised ``respond`` in asynchttpserver removing a possible "Delayed ACK" situation.
This commit is contained in:
parent
c260b22fbc
commit
cf5c8a204e
4 changed files with 166 additions and 116 deletions
|
|
@ -552,7 +552,18 @@ when defined(windows) or defined(nimdoc):
|
||||||
initAll()
|
initAll()
|
||||||
else:
|
else:
|
||||||
import selectors
|
import selectors
|
||||||
from posix import EINTR, EAGAIN, EINPROGRESS, EWOULDBLOCK, MSG_PEEK
|
when defined(windows):
|
||||||
|
import winlean
|
||||||
|
const
|
||||||
|
EINTR = WSAEINPROGRESS
|
||||||
|
EINPROGRESS = WSAEINPROGRESS
|
||||||
|
EWOULDBLOCK = WSAEWOULDBLOCK
|
||||||
|
EAGAIN = EINPROGRESS
|
||||||
|
MSG_NOSIGNAL = 0
|
||||||
|
else:
|
||||||
|
from posix import EINTR, EAGAIN, EINPROGRESS, EWOULDBLOCK, MSG_PEEK,
|
||||||
|
MSG_NOSIGNAL
|
||||||
|
|
||||||
type
|
type
|
||||||
TAsyncFD* = distinct cint
|
TAsyncFD* = distinct cint
|
||||||
TCallback = proc (sock: TAsyncFD): bool {.closure,gcsafe.}
|
TCallback = proc (sock: TAsyncFD): bool {.closure,gcsafe.}
|
||||||
|
|
@ -693,12 +704,12 @@ else:
|
||||||
|
|
||||||
proc cb(sock: TAsyncFD): bool =
|
proc cb(sock: TAsyncFD): bool =
|
||||||
result = true
|
result = true
|
||||||
let res = recv(sock.TSocketHandle, addr readBuffer[0], size,
|
let res = recv(sock.TSocketHandle, addr readBuffer[0], size.cint,
|
||||||
flags.cint)
|
flags.cint)
|
||||||
#echo("recv cb res: ", res)
|
#echo("recv cb res: ", res)
|
||||||
if res < 0:
|
if res < 0:
|
||||||
let lastError = osLastError()
|
let lastError = osLastError()
|
||||||
if lastError.int32 notin {EINTR, EWOULDBLOCK, EAGAIN}:
|
if lastError.int32 notin {EINTR, EWOULDBLOCK, EAGAIN}:
|
||||||
retFuture.fail(newException(EOS, osErrorMsg(lastError)))
|
retFuture.fail(newException(EOS, osErrorMsg(lastError)))
|
||||||
else:
|
else:
|
||||||
result = false # We still want this callback to be called.
|
result = false # We still want this callback to be called.
|
||||||
|
|
@ -708,8 +719,8 @@ else:
|
||||||
else:
|
else:
|
||||||
readBuffer.setLen(res)
|
readBuffer.setLen(res)
|
||||||
retFuture.complete(readBuffer)
|
retFuture.complete(readBuffer)
|
||||||
|
if not cb(socket):
|
||||||
addRead(socket, cb)
|
addRead(socket, cb)
|
||||||
return retFuture
|
return retFuture
|
||||||
|
|
||||||
proc send*(socket: TAsyncFD, data: string): PFuture[void] =
|
proc send*(socket: TAsyncFD, data: string): PFuture[void] =
|
||||||
|
|
@ -721,7 +732,8 @@ else:
|
||||||
result = true
|
result = true
|
||||||
let netSize = data.len-written
|
let netSize = data.len-written
|
||||||
var d = data.cstring
|
var d = data.cstring
|
||||||
let res = send(sock.TSocketHandle, addr d[written], netSize, 0.cint)
|
let res = send(sock.TSocketHandle, addr d[written], netSize.cint,
|
||||||
|
MSG_NOSIGNAL)
|
||||||
if res < 0:
|
if res < 0:
|
||||||
let lastError = osLastError()
|
let lastError = osLastError()
|
||||||
if lastError.int32 notin {EINTR, EWOULDBLOCK, EAGAIN}:
|
if lastError.int32 notin {EINTR, EWOULDBLOCK, EAGAIN}:
|
||||||
|
|
@ -734,7 +746,8 @@ else:
|
||||||
result = false # We still have data to send.
|
result = false # We still have data to send.
|
||||||
else:
|
else:
|
||||||
retFuture.complete()
|
retFuture.complete()
|
||||||
addWrite(socket, cb)
|
if not cb(socket):
|
||||||
|
addWrite(socket, cb)
|
||||||
return retFuture
|
return retFuture
|
||||||
|
|
||||||
proc acceptAddr*(socket: TAsyncFD):
|
proc acceptAddr*(socket: TAsyncFD):
|
||||||
|
|
@ -756,7 +769,8 @@ else:
|
||||||
else:
|
else:
|
||||||
register(client.TAsyncFD)
|
register(client.TAsyncFD)
|
||||||
retFuture.complete(($inet_ntoa(sockAddress.sin_addr), client.TAsyncFD))
|
retFuture.complete(($inet_ntoa(sockAddress.sin_addr), client.TAsyncFD))
|
||||||
addRead(socket, cb)
|
if not cb(socket):
|
||||||
|
addRead(socket, cb)
|
||||||
return retFuture
|
return retFuture
|
||||||
|
|
||||||
proc accept*(socket: TAsyncFD): PFuture[TAsyncFD] =
|
proc accept*(socket: TAsyncFD): PFuture[TAsyncFD] =
|
||||||
|
|
|
||||||
|
|
@ -51,10 +51,15 @@ proc `==`*(protocol: tuple[orig: string, major, minor: int],
|
||||||
proc newAsyncHttpServer*(): PAsyncHttpServer =
|
proc newAsyncHttpServer*(): PAsyncHttpServer =
|
||||||
new result
|
new result
|
||||||
|
|
||||||
proc sendHeaders*(req: TRequest, headers: PStringTable) {.async.} =
|
proc addHeaders(msg: var string, headers: PStringTable) =
|
||||||
## Sends the specified headers to the requesting client.
|
|
||||||
for k, v in headers:
|
for k, v in headers:
|
||||||
await req.client.send(k & ": " & v & "\c\L")
|
msg.add(k & ": " & v & "\c\L")
|
||||||
|
|
||||||
|
proc sendHeaders*(req: TRequest, headers: PStringTable): PFuture[void] =
|
||||||
|
## Sends the specified headers to the requesting client.
|
||||||
|
var msg = ""
|
||||||
|
addHeaders(msg, headers)
|
||||||
|
return req.client.send(msg)
|
||||||
|
|
||||||
proc respond*(req: TRequest, code: THttpCode,
|
proc respond*(req: TRequest, code: THttpCode,
|
||||||
content: string, headers: PStringTable = newStringTable()) {.async.} =
|
content: string, headers: PStringTable = newStringTable()) {.async.} =
|
||||||
|
|
@ -64,9 +69,9 @@ proc respond*(req: TRequest, code: THttpCode,
|
||||||
## This procedure will **not** close the client socket.
|
## This procedure will **not** close the client socket.
|
||||||
var customHeaders = headers
|
var customHeaders = headers
|
||||||
customHeaders["Content-Length"] = $content.len
|
customHeaders["Content-Length"] = $content.len
|
||||||
await req.client.send("HTTP/1.1 " & $code & "\c\L")
|
var msg = "HTTP/1.1 " & $code & "\c\L"
|
||||||
await sendHeaders(req, headers)
|
msg.addHeaders(customHeaders)
|
||||||
await req.client.send("\c\L" & content)
|
await req.client.send(msg & "\c\L" & content)
|
||||||
|
|
||||||
proc newRequest(): TRequest =
|
proc newRequest(): TRequest =
|
||||||
result.headers = newStringTable(modeCaseInsensitive)
|
result.headers = newStringTable(modeCaseInsensitive)
|
||||||
|
|
@ -93,90 +98,91 @@ proc sendStatus(client: PAsyncSocket, status: string): PFuture[void] =
|
||||||
|
|
||||||
proc processClient(client: PAsyncSocket, address: string,
|
proc processClient(client: PAsyncSocket, address: string,
|
||||||
callback: proc (request: TRequest): PFuture[void]) {.async.} =
|
callback: proc (request: TRequest): PFuture[void]) {.async.} =
|
||||||
# GET /path HTTP/1.1
|
|
||||||
# Header: val
|
|
||||||
# \n
|
|
||||||
var request = newRequest()
|
|
||||||
request.hostname = address
|
|
||||||
assert client != nil
|
|
||||||
request.client = client
|
|
||||||
var runCallback = true
|
|
||||||
|
|
||||||
# First line - GET /path HTTP/1.1
|
|
||||||
let line = await client.recvLine() # TODO: Timeouts.
|
|
||||||
if line == "":
|
|
||||||
client.close()
|
|
||||||
return
|
|
||||||
let lineParts = line.split(' ')
|
|
||||||
if lineParts.len != 3:
|
|
||||||
request.respond(Http400, "Invalid request. Got: " & line)
|
|
||||||
runCallback = false
|
|
||||||
|
|
||||||
let reqMethod = lineParts[0]
|
|
||||||
let path = lineParts[1]
|
|
||||||
let protocol = lineParts[2]
|
|
||||||
|
|
||||||
# Headers
|
|
||||||
var i = 0
|
|
||||||
while true:
|
while true:
|
||||||
i = 0
|
# GET /path HTTP/1.1
|
||||||
let headerLine = await client.recvLine()
|
# Header: val
|
||||||
if headerLine == "":
|
# \n
|
||||||
client.close(); return
|
var request = newRequest()
|
||||||
if headerLine == "\c\L": break
|
request.hostname = address
|
||||||
# TODO: Compiler crash
|
assert client != nil
|
||||||
#let (key, value) = parseHeader(headerLine)
|
request.client = client
|
||||||
let kv = parseHeader(headerLine)
|
var runCallback = true
|
||||||
request.headers[kv.key] = kv.value
|
|
||||||
|
|
||||||
request.reqMethod = reqMethod
|
# First line - GET /path HTTP/1.1
|
||||||
request.url = parseUrl(path)
|
let line = await client.recvLine() # TODO: Timeouts.
|
||||||
try:
|
if line == "":
|
||||||
request.protocol = protocol.parseProtocol()
|
client.close()
|
||||||
except EInvalidValue:
|
return
|
||||||
request.respond(Http400, "Invalid request protocol. Got: " & protocol)
|
let lineParts = line.split(' ')
|
||||||
runCallback = false
|
if lineParts.len != 3:
|
||||||
|
request.respond(Http400, "Invalid request. Got: " & line)
|
||||||
if reqMethod.normalize == "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.")
|
|
||||||
else:
|
|
||||||
request.body = await client.recv(contentLength)
|
|
||||||
assert request.body.len == contentLength
|
|
||||||
else:
|
|
||||||
await request.respond(Http400, "Bad Request. No Content-Length.")
|
|
||||||
runCallback = false
|
runCallback = false
|
||||||
|
|
||||||
case reqMethod.normalize
|
let reqMethod = lineParts[0]
|
||||||
of "get", "post", "head", "put", "delete", "trace", "options", "connect", "patch":
|
let path = lineParts[1]
|
||||||
if runCallback:
|
let protocol = lineParts[2]
|
||||||
await callback(request)
|
|
||||||
else:
|
|
||||||
await request.respond(Http400, "Invalid request method. Got: " & reqMethod)
|
|
||||||
|
|
||||||
# Persistent connections
|
# Headers
|
||||||
if (request.protocol == HttpVer11 and
|
var i = 0
|
||||||
request.headers["connection"].normalize != "close") or
|
while true:
|
||||||
(request.protocol == HttpVer10 and
|
i = 0
|
||||||
request.headers["connection"].normalize == "keep-alive"):
|
let headerLine = await client.recvLine()
|
||||||
# In HTTP 1.1 we assume that connection is persistent. Unless connection
|
if headerLine == "":
|
||||||
# header states otherwise.
|
client.close(); return
|
||||||
# In HTTP 1.0 we assume that the connection should not be persistent.
|
if headerLine == "\c\L": break
|
||||||
# Unless the connection header states otherwise.
|
# TODO: Compiler crash
|
||||||
await processClient(client, address, callback)
|
#let (key, value) = parseHeader(headerLine)
|
||||||
else:
|
let kv = parseHeader(headerLine)
|
||||||
request.client.close()
|
request.headers[kv.key] = kv.value
|
||||||
|
|
||||||
|
request.reqMethod = reqMethod
|
||||||
|
request.url = parseUrl(path)
|
||||||
|
try:
|
||||||
|
request.protocol = protocol.parseProtocol()
|
||||||
|
except EInvalidValue:
|
||||||
|
request.respond(Http400, "Invalid request protocol. Got: " & protocol)
|
||||||
|
runCallback = false
|
||||||
|
|
||||||
|
if reqMethod.normalize == "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.")
|
||||||
|
else:
|
||||||
|
request.body = await client.recv(contentLength)
|
||||||
|
assert request.body.len == contentLength
|
||||||
|
else:
|
||||||
|
await request.respond(Http400, "Bad Request. No Content-Length.")
|
||||||
|
runCallback = false
|
||||||
|
|
||||||
|
case reqMethod.normalize
|
||||||
|
of "get", "post", "head", "put", "delete", "trace", "options", "connect", "patch":
|
||||||
|
if runCallback:
|
||||||
|
await callback(request)
|
||||||
|
else:
|
||||||
|
await request.respond(Http400, "Invalid request method. Got: " & reqMethod)
|
||||||
|
|
||||||
|
# Persistent connections
|
||||||
|
if (request.protocol == HttpVer11 and
|
||||||
|
request.headers["connection"].normalize != "close") or
|
||||||
|
(request.protocol == HttpVer10 and
|
||||||
|
request.headers["connection"].normalize == "keep-alive"):
|
||||||
|
# In HTTP 1.1 we assume that connection is persistent. Unless connection
|
||||||
|
# header states otherwise.
|
||||||
|
# In HTTP 1.0 we assume that the connection should not be persistent.
|
||||||
|
# Unless the connection header states otherwise.
|
||||||
|
else:
|
||||||
|
request.client.close()
|
||||||
|
break
|
||||||
|
|
||||||
proc serve*(server: PAsyncHttpServer, port: TPort,
|
proc serve*(server: PAsyncHttpServer, port: TPort,
|
||||||
callback: proc (request: TRequest): PFuture[void],
|
callback: proc (request: TRequest): PFuture[void],
|
||||||
|
|
|
||||||
|
|
@ -110,12 +110,10 @@ proc recv*(socket: PAsyncSocket, size: int,
|
||||||
if socket.currPos >= socket.bufLen:
|
if socket.currPos >= socket.bufLen:
|
||||||
if (flags and MSG_PEEK) == MSG_PEEK:
|
if (flags and MSG_PEEK) == MSG_PEEK:
|
||||||
# We don't want to get another buffer if we're peeking.
|
# We don't want to get another buffer if we're peeking.
|
||||||
result.setLen(read)
|
break
|
||||||
return
|
|
||||||
let res = await socket.readIntoBuf(flags and (not MSG_PEEK))
|
let res = await socket.readIntoBuf(flags and (not MSG_PEEK))
|
||||||
if res == 0:
|
if res == 0:
|
||||||
result.setLen(read)
|
break
|
||||||
return
|
|
||||||
|
|
||||||
let chunk = min(socket.bufLen-socket.currPos, size-read)
|
let chunk = min(socket.bufLen-socket.currPos, size-read)
|
||||||
copyMem(addr(result[read]), addr(socket.buffer[socket.currPos]), chunk)
|
copyMem(addr(result[read]), addr(socket.buffer[socket.currPos]), chunk)
|
||||||
|
|
@ -181,28 +179,60 @@ proc recvLine*(socket: PAsyncSocket): PFuture[string] {.async.} =
|
||||||
## If the socket is disconnected in the middle of a line (before ``\r\L``
|
## If the socket is disconnected in the middle of a line (before ``\r\L``
|
||||||
## is read) then line will be set to ``""``.
|
## is read) then line will be set to ``""``.
|
||||||
## The partial line **will be lost**.
|
## The partial line **will be lost**.
|
||||||
|
|
||||||
template addNLIfEmpty(): stmt =
|
template addNLIfEmpty(): stmt =
|
||||||
if result.len == 0:
|
if result.len == 0:
|
||||||
result.add("\c\L")
|
result.add("\c\L")
|
||||||
|
|
||||||
result = ""
|
if socket.isBuffered:
|
||||||
var c = ""
|
result = ""
|
||||||
while true:
|
if socket.bufLen == 0:
|
||||||
c = await recv(socket, 1)
|
let res = await socket.readIntoBuf(0)
|
||||||
if c.len == 0:
|
if res == 0:
|
||||||
return ""
|
return
|
||||||
if c == "\r":
|
|
||||||
c = await recv(socket, 1, MSG_PEEK)
|
var lastR = false
|
||||||
if c.len > 0 and c == "\L":
|
while true:
|
||||||
let dummy = await recv(socket, 1)
|
if socket.currPos >= socket.bufLen:
|
||||||
assert dummy == "\L"
|
let res = await socket.readIntoBuf(0)
|
||||||
addNLIfEmpty()
|
if res == 0:
|
||||||
return
|
result = ""
|
||||||
elif c == "\L":
|
break
|
||||||
addNLIfEmpty()
|
|
||||||
return
|
case socket.buffer[socket.currPos]
|
||||||
add(result.string, c)
|
of '\r':
|
||||||
|
lastR = true
|
||||||
|
addNLIfEmpty()
|
||||||
|
of '\L':
|
||||||
|
addNLIfEmpty()
|
||||||
|
socket.currPos.inc()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
if lastR:
|
||||||
|
socket.currPos.inc()
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
result.add socket.buffer[socket.currPos]
|
||||||
|
socket.currPos.inc()
|
||||||
|
else:
|
||||||
|
|
||||||
|
|
||||||
|
result = ""
|
||||||
|
var c = ""
|
||||||
|
while true:
|
||||||
|
c = await recv(socket, 1)
|
||||||
|
if c.len == 0:
|
||||||
|
return ""
|
||||||
|
if c == "\r":
|
||||||
|
c = await recv(socket, 1, MSG_PEEK)
|
||||||
|
if c.len > 0 and c == "\L":
|
||||||
|
let dummy = await recv(socket, 1)
|
||||||
|
assert dummy == "\L"
|
||||||
|
addNLIfEmpty()
|
||||||
|
return
|
||||||
|
elif c == "\L":
|
||||||
|
addNLIfEmpty()
|
||||||
|
return
|
||||||
|
add(result.string, c)
|
||||||
|
|
||||||
proc bindAddr*(socket: PAsyncSocket, port = TPort(0), address = "") =
|
proc bindAddr*(socket: PAsyncSocket, port = TPort(0), address = "") =
|
||||||
## Binds ``address``:``port`` to the socket.
|
## Binds ``address``:``port`` to the socket.
|
||||||
|
|
|
||||||
|
|
@ -163,7 +163,7 @@ elif defined(linux):
|
||||||
proc newSelector*(): PSelector =
|
proc newSelector*(): PSelector =
|
||||||
new result
|
new result
|
||||||
result.epollFD = epoll_create(64)
|
result.epollFD = epoll_create(64)
|
||||||
result.events = cast[array[64, epoll_event]](alloc0(sizeof(epoll_event)*64))
|
#result.events = cast[array[64, epoll_event]](alloc0(sizeof(epoll_event)*64))
|
||||||
result.fds = initTable[TSocketHandle, PSelectorKey]()
|
result.fds = initTable[TSocketHandle, PSelectorKey]()
|
||||||
if result.epollFD < 0:
|
if result.epollFD < 0:
|
||||||
OSError(OSLastError())
|
OSError(OSLastError())
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue