Check Socko out on GitHub

SOCKO WEB SERVER

Socko User Guide - Basics

Introduction

Let’s deep dive into the 3 steps to Socko success:

Step 1. Define Actors and Start Akka

Socko assumes that you have your business rules implemented as Akka v2 Actors.

Incoming messages received by Socko will be wrapped within a SockoEvent and passed to your routes for dispatching to your Akka actor handlers. Your actors use SockoEvent to read incoming data and write outgoing data.

In the following HelloApp example, we have defined an actor called HelloHandler and started an Akka system called HelloExampleActorSystem. The HttpRequestEvent is used by the HelloHandler to write a response to the client.

object HelloApp extends Logger {
      //
      // STEP #1 - Define Actors and Start Akka
      // See `HelloHandler`
      //
      val actorSystem = ActorSystem("HelloExampleActorSystem")
    }
    
    /**
     * Hello processor writes a greeting and stops.
     */
    class HelloHandler extends Actor {
      def receive = {
        case event: HttpRequestEvent =>
          event.response.write("Hello from Socko (" + new Date().toString + ")")
          context.stop(self)
      }
    }

For maximum scalability and performance, you will need to carefully choose your Akka dispatchers. The default dispatcher is optimized for non blocking code. If your code blocks though reading from and writing to database and/or file system, then it is advisable to configure Akka to use dispatchers based on thread pools.

Handling Socko Events

A SockoEvent is used to read incoming and write outgoing data.

Your Actor handler must be able to handle SockoEvents.

Two ways to achieve this are:

  1. You can change your actors to be Socko SockoEvent aware by adding a SockoEvent property to messages that it receives.

  2. You can write a facade actor to specifically handle SockoEvent. Your facade can read the request from the SockoEvent in order to create messages to pass to your actors for processing. Your facade actor could store the SockoEvent to use it to write responses.

There are 4 types of SockoEvent:

  1. HttpRequestEvent

    This event is fired when a HTTP Request is received.

    To read the request, use request.content.toString() or request.content.toBytes(). Refer to the file upload example app for the decoding of HTTP post data.

    To write a response, use response.write(). If you wish to stream your response, you will need to use response.writeFirstChunk(), response.writeChunk() and response.writeLastChunk() instead. Refer to the streaming example app for usage.

  2. HttpChunkEvent

    This event is fired when a HTTP Chunk is received and is only applicable if you turn off chunk aggregation and the incoming HTTP request has the Transfer-Encoding header set to chunked.

    Reading requests and writing responses is as per HttpRequestEvent.

  3. HttpLastChunkEvent

    This event is fired when the last HTTP Chunk has been received.

    Reading requests and writing responses is as per HttpRequestEvent.

  4. WebSocketFrameEvent

    This event is fired when a Web Socket Frame is received.

    To read a frame, first check if it isText or isBinary. If text, use readText(). If binary, use readBinary().

    To write a frame, use writeText() or writeBinary().

  5. WebSocketHandshakeEvent

    This event is fired for Web Socket handshaking within your Route.

    It should not be sent to your actor.

All SockoEvents must be used by local actors only.

Akka Dispatchers and Thread Pools

Akka dispatchers controls how your Akka actors process messages.

Akka’s default dispatcher is optimized for non blocking code.

However, if your actors have blocking operations like database read/write or file system read/write, we recommend that you run these actors with a different dispatcher. In this way, while these actors block a thread, other actors can continue processing on other threads.

The following code is taken from our file upload example app. Because StaticContentHandler and FileUploadHandler actors read and write lots of files, we have set them up to use a PinnedDispatcher. Note that we have only allocated 5 threads to each processor. To scale, you may wish to allocate more threads.

val actorConfig = """
      my-pinned-dispatcher {
        type=PinnedDispatcher
        executor=thread-pool-executor
      }
      akka {
        event-handlers = ["akka.event.slf4j.Slf4jEventHandler"]
        loglevel=DEBUG
        actor {
          deployment {
            /static-file-router {
              router = round-robin
              nr-of-instances = 5
            }
            /file-upload-router {
              router = round-robin
              nr-of-instances = 5
            }
          }
        }
      }"""

    val actorSystem = ActorSystem("FileUploadExampleActorSystem", ConfigFactory.parseString(actorConfig))

    val staticContentHandlerRouter = actorSystem.actorOf(Props[StaticContentHandler]
      .withRouter(FromConfig()).withDispatcher("my-pinned-dispatcher"), "static-file-router")
    
    val fileUploadHandlerRouter = actorSystem.actorOf(Props[FileUploadHandler]
      .withRouter(FromConfig()).withDispatcher("my-pinned-dispatcher"), "file-upload-router")

Step 2. Define Routes

Routes allows you to control how Socko dispatches incoming events to your actors.

Routes are implemented in Socko using partial functions that take a SockoEvent as input and returns Unit (or void).

Within your implementation of the partial function, your code will need to dispatch the SockoEvent to your intended actor for processing.

To assist with dispatching, we have included pattern matching extractors:

Concatenation of 2 or more extractors is also supported.

The following example illustrates matching HTTP GET event and dispatching it to a HelloHandler actor:

val routes = Routes({
      case GET(request) => {
        actorSystem.actorOf(Props[HelloHandler]) ! request
      }
    })

For a more detailed example, see our example route app.

Event Extractors

These extractors allows you to match different types of SockoEvent.

The following code taken from our web socket example app illustrates usage:

val routes = Routes({
    
      case HttpRequest(httpRequest) => httpRequest match {
        case GET(Path("/html")) => {
          // Return HTML page to establish web socket
          actorSystem.actorOf(Props[WebSocketHandler]) ! httpRequest
        }
        case Path("/favicon.ico") => {
          // If favicon.ico, just return a 404 because we don't have that file
          httpRequest.response.write(HttpResponseStatus.NOT_FOUND)
        }
      }
      
      case WebSocketHandshake(wsHandshake) => wsHandshake match {
        case Path("/websocket/") => {
          // To start Web Socket processing, we first have to authorize the handshake.
          // This is a security measure to make sure that web sockets can only be established at your specified end points.
          wsHandshake.authorize()
        }
      }
    
      case WebSocketFrame(wsFrame) => {
        // Once handshaking has taken place, we can now process frames sent from the client
        actorSystem.actorOf(Props[WebSocketHandler]) ! wsFrame
      }
    
    })

Host Extractors

Host extractors match the host name received in the HTTP request that triggered the SockoEvent.

For HttpRequestEvent, the host is the value specified in the HOST header variable. For HttpChunkEvent, WebSocketFrameEvent and WebSocketHandshakeEvent, the host is that of the associated initial HttpRequestEvent.

For example, the following HTTP request has a host value of www.sockoweb.org:

GET /index.html HTTP/1.1
Host: www.sockoweb.org

Host

Performs an exact match on the specified host.

The following example will match www.sockoweb.org but not: www1.sockoweb.org, sockoweb.com or sockoweb.org.

val r = Routes({
      case Host("www.sockoweb.org") => {
        ...
      }
    })

HostSegments

Performs matching and variable binding on segments of a host. Each segment is assumed to be delimited by a period.

For example:

val r = Routes({
      // Matches server1.sockoweb.org
      case HostSegments(server :: "sockoweb" :: "org" :: Nil) => {
        ...
      }
    })

This will match any hosts that have 3 segments and the last 2 segments being sockoweb.org. The first segment will be bound to a variable called server.

This will match www.sockoweb.org and the server variable have a value of www.

This will NOT match www.sockoweb.com because it ends in .com; or sockweb.org because there are only 2 segments.

HostRegex

Matches the host based on a regular expression pattern.

For example, to match www.anydomainname.com, first define your regular expression as an object and then use it in your route.

object MyHostRegex extends HostRegex("""www\.([a-z]+)\.com""".r)
    
    val r = Routes({
      // Matches www.anydomainname.com
      case MyHostRegex(m) => {
        assert(m.group(1) == "anydomainname")
        ...
      }
    })

Method Extractors

Method extractors match the method received in the HTTP request that triggered the SockoEvent.

For HttpRequestEvent, the method is the extracted from the 1st line. For HttpChunkEvent, WebSocketFrameEvent and WebSocketHandshakeEvent, the method is that of the associated initial HttpRequestEvent.

For example, the following HTTP request has a method GET:

GET /index.html HTTP/1.1
Host: www.sockoweb.org

Socko supports the following methods:

For example, to match every HTTP GET:

val r = Routes({
      case GET(_) => {
        ...
      }
    })

Method extractors return the SockoEvent so it can be used with other extractors in the same case statement.

For example, to match HTTP GET with a path of “/clients”

val r = Routes({
      case GET(Path("/clients")) => {
        ...
      }
    })

Path Extractors

Path extractors match the path received in the HTTP request that triggered the SockoEvent.

For HttpRequestEvent, the path is the extracted from the 1st line without any query string. For HttpChunkEvent, WebSocketFrameEvent and WebSocketHandshakeEvent, the path is that of the associated initial HttpRequestEvent.

For example, the following HTTP requests have a path value of /index.html:

GET /index.html HTTP/1.1
Host: www.sockoweb.org

GET /index.html?name=value HTTP/1.1
Host: www.sockoweb.org

Path

Performs an exact match on the specified path.

The following example will match folderX but not: /folderx, /folderX/ or /TheFolderX.

val r = Routes({
      case Path("/folderX") => {
        ...
      }
    })

PathSegments

Performs matching and variable binding on segments of a path. Each segment is assumed to be delimited by a slash.

For example:

val r = Routes({
      // Matches /record/1
      case PathSegments("record" :: id :: Nil) => {
        ...
      }
    })

This will match any paths that have 2 segments and the first segment being record. The second segment will be bound to a variable called id.

This will match /record/1 and id will be set to 1. This will NOT match /record because there is only 1 segment; or /folder/1 before the first segment is not record.

Another example:

val r = Routes({
      // Matches /api/abc and /api/xyz
      case PathSegments("api" :: relativePath) => {
        ...
      }
    })

This will match any path that has the first segment set as api. The remainder of the path segments will be boudn to the variable relativePath.

This will match /api/abc and relativePath will be set to List(abc). This will NOT match /aaa/abc because the first segment is not api.

PathRegex

Matches the path based on a regular expression pattern.

For example, to match /path/to/file, first define your regular expression as an object and then use it in your route.

object MyPathRegex extends PathRegex("""/path/([a-z0-9]+)/([a-z0-9]+)""".r)
    
    val r = Routes({
      // Matches /path/to/file
      case MyPathRegex(m) => {
        assert(m.group(1) == "to")
        assert(m.group(2) == "file")
        ...
      }
    })

Query String Extractors

Query string extractors match the query string received in the HTTP request that triggered the SockoEvent.

For HttpRequestEvent, the query string is the extracted from the 1st line. For HttpChunkEvent, WebSocketFrameEvent and WebSocketHandshakeEvent, the query string is that of the associated initial HTTP Request.

For example, the following HTTP request has a query string value of name=value:

GET /index.html?name=value HTTP/1.1
Host: www.sockoweb.org

QueryString

Performs an exact match on the query string.

The following example will match action=save but not: action=view or action=save&id=1.

val r = Routes({
      case QueryString("action=save") => {
        ...
      }
    })

QueryStringField

Performs matching and variable binding a query string value for a specified query string field.

For example, to match whenever the action field is present and bind its value to a variable called actionValue:

object ActionQueryStringField extends QueryStringName("action")
    
    val r = Routes({
      // Matches '?action=save' and actionValue will be set to 'save'
      case ActionQueryStringField(actionValue) => {
        ...
      }
    })

QueryStringRegex

Matches the query string based on a regular expression pattern.

For example, to match ?name1=value1:

object MyQueryStringRegex extends QueryStringRegex("""name1=([a-z0-9]+)""".r)
    
    val r = Routes({
      // Matches /path/to/file
      case MyQueryStringRegex(m) => {
        assert(m.group(1) == "value1")
        ...
      }
    })

Concatenation Extractors

At times, it is useful to combine 2 or more extractors in a single case statement. For this, you can use an ampersand (&).

For example, if you wish to match a path of /record/1 and a query string of action=save, you can use

object ActionQueryStringField extends QueryStringName("action")
    
    val r = Routes({
      case PathSegments("record" :: id :: Nil) & ActionQueryStringField(actionValue) => {
        ...
      }
    })

Step 3. Start/Stop Web Server

To start you web server, you only need to instance the WebServer class and call start() passing in your configuration and routes. When you wish to stop the web server, call stop().

For example, for a Scala console application:

def main(args: Array[String]) {
      val webServer = new WebServer(WebServerConfig(), routes)
      webServer.start()
  
      Runtime.getRuntime.addShutdownHook(new Thread {
        override def run { webServer.stop() }
      })
    
      System.out.println("Open your browser and navigate to http://localhost:8888")
    }

For a AKKA microkernel application:

class SockoKernel extends Bootable {
      val system = ActorSystem("sockokernel")
      val webServer = new WebServer(WebServerConfig(), routes)

      def startup = {
        webServer.start()
        System.out.println("Open your browser and navigate to http://localhost:8888")
      }
     
      def shutdown = {
        webServer.stop()
      }
    }

These example uses the default configuration which starts the web server at localhost bound on port 8888. To customise, refer to Configuration.