guildenstern/guildenserver

GuildenServer is the abstract base class for upstream (app-facing) web servers. The three concrete server implementations that currently ship with GuildenStern are guildenstern/httpserver, guildenstern/websocketserver and guildenstern/multipartserver. One server is associated with one TCP port.

GuildenServer mainly acts as the glue between everything else, offering set of callback hooks for others to fill in. In addition to GuildenServer, this module also introduces SocketContext, which is a container for data of one request in flight. SocketContext is inheritable, so concrete servers may add properties to it.

The overall architecture may be something like this: A reverse proxy (like https://caddyserver.com/) routes requests upstream to multiple ports. Each of these ports is served by one concrete GuildenServer instance. To each server is attached one dispatcher, which listens to the port and triggers handlerCallbacks. The default guildenstern/dispatcher uses multithreading so that even requests arriving to the same port are served in parallel. During request handling, the default servers offer an inherited thread local SocketContext variable from which everything else is accessible, most notably the SocketData.server itself and the SocketData.socket that is being serviced.

Guides for writing your very own servers and dispatchers may appear later. For now, just study the source codes... (And if you invent something useful, please share it with us.)

Types

CloseOtherSocketCallback = proc (server: GuildenServer; socket: SocketHandle;
                                 cause: SocketCloseCause; msg: string = "") {.
    ...gcsafe, nimcall, ...raises: [].}
CloseSocketCallback = proc (socketdata: ptr SocketData; cause: SocketCloseCause;
                            msg: string) {....gcsafe, nimcall, ...raises: [].}
GuildenServer {.inheritable.} = ref object
  port*: uint16
  thread*: Thread[ptr GuildenServer]
  id*: int
  logCallback*: LogCallback
  loglevel*: LogLevel
  started*: bool
  internalThreadInitializationCallback*: ThreadInitializerCallback
  threadInitializerCallback*: ThreadInitializerCallback
  handlerCallback*: HandlerCallback
  suspendCallback*: SuspendCallback
  closeSocketCallback*: CloseSocketCallback
  closeOtherSocketCallback*: CloseOtherSocketCallback
  onCloseSocketCallback*: OnCloseSocketCallback
HandlerCallback = proc (socketdata: ptr SocketData) {.nimcall, ...gcsafe,
    raises: [].}
LogCallback = proc (loglevel: LogLevel; message: string) {....gcsafe, nimcall,
    ...raises: [].}
LogLevel = enum
  TRACE, DEBUG, INFO, NOTICE, WARN, ERROR, FATAL, NONE
OnCloseSocketCallback = proc (socketdata: ptr SocketData;
                              cause: SocketCloseCause; msg: string) {....gcsafe,
    nimcall, ...raises: [].}
SocketCloseCause = enum
  Excepted = -1000,         ## A Nim exception happened
  CloseCalled,              ## Use this, when the server (your code) closes a socket
  AlreadyClosed,            ## Another thread has closed the socket
  ClosedbyClient,           ## Client closed the connection
  ConnectionLost,           ## TCP/IP connection was dropped
  TimedOut,                 ## Client did not send/receive all expected data
  ProtocolViolated,         ## Client was sending garbage
  NetErrored,               ## Some operating system level error happened
  SecurityThreatened,       ## Use this, when you decide to close socket for security reasosns 
  DontClose                  ## Internal flag
Parameter in close callbacks.
SocketContext {.inheritable.} = ref object
  socketdata*: ptr SocketData
SocketData = object
  server*: GuildenServer
  socket*: SocketHandle
  isserversocket*: bool
  flags*: int
  customdata*: pointer

Data associated with every incoming socket message.
This is available in SocketContext via socketdata pointer.
customdata pointer can be freely used in user code.

SuspendCallback = proc (server: GuildenServer; sleepmillisecs: int) {.nimcall,
    ...gcsafe, raises: [].}
ThreadInitializerCallback = proc (server: GuildenServer) {.nimcall, ...gcsafe,
    raises: [].}

Vars

shutdownevent = newSelectEvent()
shuttingdown = false
Global variable that all code is expected to observe and abide to.
socketcontext {.threadvar.}: SocketContext

Procs

proc `$`(x: SocketHandle): string {.inline, ...raises: [], tags: [], forbids: [].}
proc closeOtherSocket(server: GuildenServer; socket: posix.SocketHandle;
                      cause: SocketCloseCause = CloseCalled; msg: string = "") {.
    ...gcsafe, nimcall, ...raises: [], tags: [RootEffect], forbids: [].}
Call this to close an open socket that is not the socket currently being served.
proc closeSocket(cause = CloseCalled; msg = "") {....gcsafe, nimcall, ...raises: [],
    tags: [RootEffect], forbids: [].}
Call this to close a socket yourself.
proc initialize(server: GuildenServer; loglevel: LogLevel) {....raises: [],
    tags: [], forbids: [].}
proc shutdown() {....raises: [], tags: [], forbids: [].}
Sets shuttingdown to true and signals dispatcher loops to cease operation.
proc suspend(sleepmillisecs: int) {.inline, ...raises: [], tags: [RootEffect],
                                    forbids: [].}
Instead of os.sleep, use this. This informs a dispatcher that your thread is waiting for something and therefore another thread should be allowed to run.

Templates

template handleRead(socketdata: ptr SocketData)
template initializeThread(server: ptr GuildenServer)
template log(theserver: GuildenServer; level: LogLevel; message: string)
Calls logCallback, if it set. By default, the callback is set to echo the message, if level is same or higher than server's loglevel.