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:
Dominik Picheta 2014-07-12 22:51:06 +01:00
commit cf5c8a204e
4 changed files with 166 additions and 116 deletions

View file

@ -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] =

View file

@ -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],

View file

@ -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.

View file

@ -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())