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.
Trait HServer provides:
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.
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.
final def start: Unit
- self explained.
def stop: Unit
- self explained.
Presents something called 'application' - a group of request handlers (HLets) sharing common behaviour.
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.
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 }
This is your request handler's gate to the world. Some methods return this for chaining. HTalk public interface consists of:
val req: HReqData
See HReqData above.
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.
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)
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 }
It is a request handler.
def act(talk : HTalk) : Unit
- request handling. Almost all your code is going here.
Just use HTalk.
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).
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.
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")
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.
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.