tiscaf http server, manual

This is a tiscaf http server manual. I hope this very short manual you are reading is both easy readable and complete. And be sure it is up to date.

HServer

Trait HServer provides:

to implement

protected def apps : Seq[HApp] - you must supply a list of web applications (see HApp below). Take in mind, an order of applications in the list is important for dispatching (is explained below also).

protected def ports : Set[Int] - ports to listen to. Dedicated TCP connection acceptor will be started in own thread for each port.

to override

protected def name = "tiscaf" - server's name used in response header

protected def stopPort : Int = 8911 - at server start listener to this port is also started, waiting for "stop" command to shutdown. Hasn't any sense if you override startStopListener method (see below).

protected def poolSize : Int = Runtime.getRuntime.availableProcessors * 2 - the executor pool size.

protected def queueSize : Int = Int.MaxValue - the executor queue size.

def bufferSize : Int = 4096 - nio buffers size. Increase it at case of frequent multi-MB downloads and uploads. It is public as far as can be used in FsLets (see below).

protected def tcpNoDelay : Boolean = false - TCP socket's parameter. It is false by default. The only reason to set to true is the case of benchmarking of a single (or few) clients (say, you can get ~25 requests per second only for single-thread client with not-persistent connection when false is set). Set the parameter to false in production to make TCP/IP stack more happy.

def connectionTimeoutSeconds : Int = 20 - it has two purposes:

protected def maxPostDataLength : Int = 65536 - self explained.

def interruptTimeoutMillis : Int = 1000 - after the server has got 'stop' command, this period will be given to handlers to terminate their work before interrupting them.

protected def onError(e : Throwable) : Unit - you can delegate error handling to your favourite logging system. Probably, some kind of filtering may be useful - say, when client interrupts a connection, you may get 'broken pipe' or something such.

protected def startStopListener : Unit - the method is calling during server starting. By default it starts (in dedicated thread) a primitive port listener which waits for 'stop' sequence from HStop.stop. If you need more elaborated shutting down procedure, you can override the method and write your own 'stopper' and stop-listener.

API

final def start: Unit - self explained.

def stop: Unit - self explained.

HApp

Presents something called 'application' - a group of request handlers (HLets) sharing common behaviour.

to implement

def resolve(req : HReqData) : Option[HLet] - core dispatching method, returning a handler to process the request. To decide which handler to use, you have full request information presented by HReqData:

trait HReqData {
  // common
  def method: HReqType.Value
  def host: Option[String]
  def port: Option[String]
  def uriPath: String
  def uriExt: Option[String]
  def query: String
  def remoteIp: String
  def contentLength: Option[Long]

  // header
  def header(key: String): Option[String]
  def headerKeys: scala.collection.Set[String]

  // parameters
  def paramsKeys: Set[String]
  def params(key: String): Seq[String]
  def param(key: String): Option[String]
  def softParam(key: String): String

  def asQuery(ignore: Set[String] = Set()): String

  // param(key) helpers
  def asByte(key: String): Option[Byte]
  def asShort(key: String): Option[Short]
  def asInt(key: String): Option[Int]
  def asLong(key: String): Option[Long]
  def asFloat(key: String): Option[Float]
  def asDouble(key: String): Option[Double]

  // POST/application/octet-stream case
  def octets: Option[Array[Byte]]
}

uriExt is an URI path extension - when you use sessions with URL-rewriting, you have URIs like '.../index.html;sid=bla-bla-bla', where 'sid=bla-bla-bla' is an uriExt.

All (request and response) headers keys are case-insensitive.

HReqType is defined as

object HReqType extends Enumeration {
  val Invalid    = Value("Invalid")
  val Get        = Value("GET")
  val PostData   = Value("POST/application/x-www-form-urlencoded")
  val PostOctets = Value("POST/application/octet-stream")
  val PostMulti  = Value("POST/multipart/form-data")
}

Request parser falls back to HReqType.PostOctets type when content type is another rather application/x-www-form-urlencoded or multipart/form-data, delegating binary data stream processing to handler.

As I have already said, HServer.apps list order is important. Instead of plenty of words, let's see how dispatching works:

protected object HResolver {

  private object errApp extends HApp {
    // some code to define the HApp
    val hLet = new let.ErrLet(HStatus.NotFound)
  }
  
  def resolve(apps: Seq[HApp], req : HReqData) : (HApp, HLet) = {
    @scala.annotation.tailrec
    def doFind(rest: Seq[HApp]): (HApp,HLet) = rest match {
      case Seq()      => (errApp, errApp.hLet)
      case Seq(a, _*) => a.resolve(req) match {
        case Some(let) => (a, let)
        case None      => doFind(rest.tail)
      }
    }
    doFind(apps)
  }
}

You see, if resolver has not found suitable (HApp, HLet) pair, default 'not found' response will be responded.

You can have last HApp in HServer.apps list with your own 'not found' handler (or, say, return 'Charlie Parker - Summertime.ogg'). Inside each HApp.resolve you will probably have some kind of matching against request parameters, and can end up with FsLet (supplied handler to deal with static content) to access images, css, js and other such resources. Also, you can... Ugh, you see, you can everything wrt dispatching. Also see HTree below to deal with (may be partially) tree-like dispatching.

to override

Also HApp has few params you can override:

  def tracking: HTracking.Value  = HTracking.NotAllowed
  def sessionTimeoutMinutes: Int = 15
  def maxSessionsCount: Int      = 500
  def keepAlive: Boolean         = true
  def chunked: Boolean           = false
  def buffered: Boolean          = false 
  def gzip: Boolean              = false
  def encoding: String           = "UTF-8" // for request params and strings output
  def cookieKey: String          = "TISCAF_SESSIONID"
  def sidKey: String             = "sid"

They are self-explained. If you are not using buffered (or gzipped as a case of buffered) or chunked output, you need to set content length manually in request handler.

Session HTracking is defined as:

object HTracking extends Enumeration { 
  val NotAllowed, Uri, Cookie = Value
}

HTalk

This is your request handler's gate to the world. Some methods return this for chaining. HTalk public interface consists of:

request data

  val req: HReqData

See HReqData above.

setting status response header

  def setStatus(code: HStatus.Value): HTalk
  def setStatus(code: HStatus.Value, msg: String): HTalk
  
  def setHeader(name: String, value: String): HTalk
  def removeHeader(name: String): Option[String]
  def getHeader(name: String): Option[String]
  
  def setContentLength(length: Long): HTalk
  def setCharacterEncoding(charset: String): HTalk
  def setContentType(cType: String): HTalk

Self-explained again. Recall, all headers-related keys are case-insensitive.

output

  def encoding: String

- goes from HApp.encoding, is used both for request params decoding and generating output bytes from strings.

  def write(ar: Array[Byte], offset: Int, length: Int): HTalk
  def write(ar: Array[Byte]): HTalk
  def bytes(s: String): Array[Byte] // uses HApp.encoding
  def write(s: String): HTalk       // uses bytes(s)

session

Implements mutable map.

  object ses extends scala.collection.mutable.Map[Any,Any] {
    // implementing mutable.Map
    ...
    ...

    // Map-related helpers
    def asString(key: Any): Option[String]
    def asBoolean(key: Any): Option[Boolean]
    def asByte(key: Any): Option[Byte]
    def asShort(key: Any): Option[Short]
    def asInt(key: Any): Option[Int]
    def asLong(key: Any): Option[Long]
    def asFloat(key: Any): Option[Float]
    def asDouble(key: Any): Option[Double]
    def asDate(key: Any): Option[Date]

    def clearKeeping(keysToKeep: Any*): Unit

    // session-specific
    def tracking: HTracking.Value
    def isAllowed: Boolean
    def isValid: Boolean
    def invalidate: Unit
    
    def idKey: String
    def id: String
    def idPhrase: String
  }

HLet

It is a request handler.

to implement

def act(talk : HTalk) : Unit - request handling. Almost all your code is going here. Just use HTalk.

to override

def talkExecutor: Option[HLetExecutor] = None where

trait HLetExecutor {
  def submit(req: HReqData, run: Runnable)
}

Overriding this method you can change handler execution environment the way you want. Default execution environment is presented by execution pool you configure in HServer implementation. Again you have full request information in HReqData, which is explained in HApp chapter.

At case a handler has potentially blocking behavior, say, you have event-subscribe framework, you can execute Runnable in your event handler. In fact, this Runnable consists of HLet.act call with some pre- and post-code.

def partsAcceptor(reqInfo: HReqHeaderData): Option[HPartsAcceptor] = None - is used for handling multipart request, which is executed before act. HPartsAcceptor looks like

// any returned 'false' disposes the connection, declineAll will not be 
// called - i.e. acceptor must do all cleanup work itself at 'false'

abstract class HPartsAcceptor(reqInfo: HReqHeaderData) {
  
  // to implement (callbacks)
  def open(desc: HPartDescriptor): Boolean // new part starts with it...
  def accept(bytes: Array[Byte]): Boolean  // ... takes bytes (multiple calls!)...
  def close: Unit                          // ... and ends with this file...
  def declineAll: Unit                     // ... or this one apeals to abort all parts
  
  // to override
  def headerEncoding: String = "UTF-8"
}

where HPartDescriptor describes each part:

trait HPartDescriptor {
  def header(key: String): Option[String]
  def headerKeys: scala.collection.Set[String]
  override def toString = ..something readable..
}

As you can imagine, for each part open, accept (few times) and close methods will be called during request parsing. If you want to decline this data stream (say, client doesn't respect max upload size), just return false - the connection will be closed.

Also a request parser may deside input stream is illegal and call declineAll, which you can use to clean up any resources (say, close files).

API

There are few helper methods you can use during talking. Some of them are self-explained:

  protected def error(status: HStatus.Value, msg: String, tk: HTalk) = new let.ErrLet(status, msg) act(tk)
  protected def error(status: HStatus.Value, tk: HTalk)              = new let.ErrLet(status) act(tk)
  protected def e404(tk: HTalk)                                      = error(HStatus.NotFound, tk)

Some of helpers need few words:

protected def redirect(to: String, tk: HTalk) = new let.RedirectLet(to) act(tk) - uses helper handler - RedirectLet - which act method is:

  def act(tk: HTalk) {
    tk.setContentLength(0)
      .setContentType("text/html")
      .setHeader("Location", toUrl)
      .setStatus(HStatus.MovedPermanently)
      .close
  }

You see, just Location header is used.

protected def sessRedirect(to: String, tk: HTalk): Unit - the same as redirect, but inserts URI path extension into the target URI.

FsLet

There are few HLets located in the let package. You have already seen ErrLet and RedirectLet. There is another useful handler which handles requests to static (file system based) content. It is (surprise?) FsLet handler:

trait FsLet extends HLet {
  
  //----------------- to implement -------------------------
  
  protected def dirRoot: String // will be mounted to uriRoot

  //----------------- to override -------------------------
  
  protected def uriRoot: String         = ""  // say, "myKit/theDir"
  protected def indexes: Seq[String]    = Nil // say, List("index.html", "index.htm")
  protected def allowLs: Boolean        = false
  protected def bufSize: Int            = 4096
  protected def plainAsDefault: Boolean = false
  
  // internals...
}

To implement:

protected def dirRoot: String - it is your file system path to static content root directory, say, '/home/thelonious/my-site/img'.

To override:

protected def uriRoot: String = "" - this is an URI path prefix the dirRoot will be mounted to. Say, "img", or "", or "a/b/c" (warning for Microsoft Windows users: please, follow standards and do not use back slash here).

protected def indexes: Seq[String] = stdIndexes - self explained (see FsLet.stdIndexes above). Probably, you will want Nil here.

protected def allowLs: Boolean = false - if true, standard directory listing html will be returned for directory, as you probably have noticed playing with HomeServer demo.

protected def bufSize: Int = 4096 - files are read to Array[Byte] with this size, then the buffer is used in HTalk.write call.

protected def plainAsDefault: Boolean = false - few MIME types are listed inside HData.scala. At case a type is unknown, this code takes place:

  if(plainAsDeault) tk.setContentType("text/plain")
  else tk.setContentType("application/octet-stream") 

ResourceLet

This trait is similar to FsLet, but has the aim to access java resources - say, files inside jars for jars in classpath. Enclosing HApp must has bufeered or chunked set to true.

trait ResourceLet  extends HLet {

  //----------------- to implement -------------------------

  protected def dirRoot: String // will be mounted to uriRoot

  //----------------- to override -------------------------

  protected def getResource(path: String): java.io.InputStream = ... (own implementation to deal with jars)
  protected def uriRoot: String         = "" // say, "myKit/theDir"
  protected def indexes: Seq[String]    = Nil
  protected def bufSize: Int            = 4096
  protected def plainAsDefault: Boolean = false

  // internals...
}

You see, it is possible to override getResource(path: String) method to access other kinds of tree-like resources if you have invented them.

HTree

If your application has (at least partly) tree-like structure (uri nodes correspond to request handlers), you can assign handlers to tree nodes in eyes-friendly manner:

object HTree {
  def stub(text: String) : HLet = new HLet { ... }
}

trait HTree {

  def dir: String       = ""
  def let: Option[HLet] = None
  def lays: Seq[HTree]  = Nil

  final def !(addLet: => HLet) = new HTree { ... }
  final def +=(addLays: HTree*) = new HTree { ... }
  final def resolve(dirs: Seq[String]): Option[HLet] = ...
  final def resolve(uriPath: String): Option[HLet] = resolve(uriPath.split("/"))
}

Instead of long and vague explanation let's look at this example:

object MngApp extends HApp {

  def resolve(req: HReqHeaderData): Option[HLet] = admRoot.resolve(req.uriPath)

  private lazy val bookkeepers =
    "bookkeeper" += (
      "new"  ! stub("new bookkeeper - not implemented yet"),
      "list" ! adm.bk.ListBkLet
    )

  private lazy val admRoot =
    "adm" += ( // adm root hasn't a handler. If has: "adm" ! adm.RootLet += (
      "in" ! adm.InLet, // "domain.com/adm/in"
      "menu" ! adm.MenuLet,
      "manager" += (
        "new"  ! adm.man.NewManLet,
        "list" ! adm.man.ListManLet
      ),
      bookkeepers
    )

  // ...
}

This example mostly uses objects (rather classes instances) as handlers (and it is coommon case in my practice). During development period, when some of handlers still are not implemented, you can use HTree.stub(text : String) method. In fact you can start with stubs only, and replace them with real handlers step by step. And, as you can see with bookkeepers inside admRoot, you can nest subtrees in a way you want.