guildenstern/guildenserver

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

GuildenServer mainly acts as a connection 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.

So, the overall architecture may be something like this: A reverse proxy (like https://caddyserver.com/) routes requests 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 being serviced.

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

Example

(In this example, port number is hardcoded into html just for demonstration purposes. In reality, use your reverse proxy to route requests to different ports.)

# nim r --d:threadsafe thisexample

import cgi, guildenstern/[dispatcher, httpserver]

proc handleGet() =
  let html = """
    <!doctype html><title>GuildenStern Example</title><body>
    <form action="http://localhost:5051" method="post">
    <input name="say" id="say" value="Hi"><button>Send"""
  reply(Http200, html)

proc handlePost() =
  try:
    echo readData(getBody()).getOrDefault("say")
    reply(Http303, ["location: " & http.headers.getOrDefault("origin")])
  except: reply(Http500)

let getServer = newHttpServer(handleGet)
let postServer = newHttpServer(handlePost)
getServer.start(5050)
postServer.start(5051)
echo "getServer serving at localhost:5050"
joinThreads(getServer.thread, postServer.thread)

Types

GuildenServer {.inheritable.} = ref object
  loglevel*: LogLevel
  port*: uint16
  thread*: Thread[ptr GuildenServer]
  logCallback*: LogCallback
  onCloseSocketCallback*: OnCloseSocketCallback
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*: posix.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.

Vars

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

Procs

proc closeOtherSocket(server: GuildenServer; socket: posix.SocketHandle;
                      cause: SocketCloseCause = CloseCalled; msg: string = "") {.
    ...gcsafe, nimcall, ...raises: [], tags: [], 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: [], forbids: [].}
Call this to close a socket yourself.
proc shutdown() {....raises: [], tags: [], forbids: [].}
Sets shuttingdown to true and signals dispatcher loops to cease operation.
proc suspend(sleepmillisecs: int) {.inline, ...raises: [], tags: [], 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 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.