Check Socko out on GitHub

SOCKO WEB SERVER

Socko User Guide - Serving Dynamic HTTP Content

Introduction

Parsing Data

Parsing Query Strings

You can access query string parameters using HttpRequestEvent.

// For mypath?a=1&b=2
  val qsMap = event.endPoint.queryStringMap
  assert (qsMap("a") == "1")

See the query string and post data example for more details.

Parsing Post Form Data

If you do not have to support file uploads and your post form data content type is application/x-www-form-urlencoded data, you can access the form data using HttpRequestEvent.

// For firstName=jim&lastName=low
  val formDataMap = event.request.content.toFormDataMap
  assert (formDataMap("firstName") == "jim")

See the query string and post data example for more details.

Parsing File Uploads

If you intend to support file uploads, you need to use Netty’s HttpPostRequestDecoder.

The following example extracts the description field as well as a file that was posted.

//
  // The following form has a file upload input named "fileUpload" and a description
  // field named "fileDescription".
  //
  // ------WebKitFormBoundaryThBHDfQBdTlMy3sK
  // Content-Disposition: form-data; name="fileUpload"; filename="myfile.txt"
  // Content-Type: text/plain
  // 
  // file contents
  // ------WebKitFormBoundaryThBHDfQBdTlMy3sK
  // Content-Disposition: form-data; name="fileDescription"
  // 
  // this is my file upload
  // ------WebKitFormBoundaryThBHDfQBdTlMy3sK--
  //

  val decoder = new HttpPostRequestDecoder(HttpDataFactory.value, event.nettyHttpRequest)
  val descriptionField = decoder.getBodyHttpData("fileDescription").asInstanceOf[Attribute]
  val fileField = decoder.getBodyHttpData("fileUpload").asInstanceOf[FileUpload]
  val destFile = new File(msg.saveDir, fileField.getFilename)

See the file upload example for more details.

RESTful Web Services

Socko’s RestHandler class provides a quick way to expose your Akka actors as REST end points. You will be able to invoke your Akka actors using Javascript in a web browser app and from other HTTP clients.

It also natively supports Swagger. With Swagger, you will be able to provide your users with your API documentation straight from your code.

The current limitations of Socko’s RestHandler are: - Only JSON is supported. XML is not supported. - Only HTTP is supported. Web Socket is not supported. - This module relies on Scala reflection. Reflection is tagged as experimental in Scala 2.10. There is also an issue with thread safety. For unit tests to work, you have to execute them serially (in sbt set parallelExecution in Test := false). Also, instancing RestRegistry in Step #5 must be performed in a single thead.

To use the RestHandler class, follow the 5 steps below. The example code can be found in the Pet Shop example REST app.

Step 1. Installation

In order to use the Socko RESTful Web Service framework, you need to add an extra dependency.

Add the following to your build.sbt. Replace X.Y.Z with the version number.

libraryDependencies += "org.mashupbots.socko" %% "socko-rest" % "X.Y.Z"

Step 2. Implement your Data Model

Define your model as case classes.

case class Tag(id: Long, name: String)
  case class Category(id: Long, name: String)
  case class Pet(id: Long, category: Category, name: String, photoUrls: List[String], tags: List[Tag], status: String)

You can specify additional information to describe the properties in your data model using RestModelMetaData. For example, to add more information about a Pet, create a companion Pet object that extends RestModelMetaData. Then, provide additional meta data using RestPropertyMetaData.

object Pet extends RestModelMetaData {
    val modelProperties = Seq(
      RestPropertyMetaData("name", "Name of the pet"),
      RestPropertyMetaData("status", "pet status in the store", Some(AllowableValuesList(List("available", "pending", "sold")))))
  }

Step 3. Implement your Business Logic Actors

Socko RestHandler uses a Request/Response model.

Incoming request data is deserialized into RestRequest. Similarly, an outgoing RestResponse is serialized into response data.

The following is an example implementation of a REST operation that returns a Pet given the pet’s id as input.

case class GetPetRequest(context: RestRequestContext, petId: Long) extends RestRequest
  case class GetPetResponse(context: RestResponseContext, pet: Option[Pet]) extends RestResponse

  class PetProcessor() extends Actor with akka.actor.ActorLogging {
    def receive = {
      case req: GetPetRequest =>
        val pet = PetData.getPetById(req.petId)
        val response = if (pet != null) {
          GetPetResponse(req.context.responseContext, Some(pet))
        } else {
          GetPetResponse(req.context.responseContext(404), None)
        }
        sender ! response
        context.stop(self)
    }
  }

Your request and response must extend from RestRequest and RestResponse respectively as illustrated in GetPetRequest and GetPetResponse. Your request and resposne must declare context as the first parameter. Subsequement parameters can contain primitives and your data model classes.

Step 4. Register your REST Operation

In order to notify RestHandler to the existance of your actor, you need to regsiter it.

Registering involves creating a Scala object that extends RestRegistration.

At minimum, you must provide the method, path, bindings for your request parameters and the actor that you wish to use to process incoming requests.

object GetPetRegistration extends RestRegistration {
    val method = Method.GET
    val path = "/pet/{petId}"
    val requestParams = Seq(PathParam("petId", "ID of pet that needs to be fetched"))
    def processorActor(actorSystem: ActorSystem, request: RestRequest): ActorRef = actorSystem.actorOf(Props[PetProcessor])
  }

You can also specify additional information to describe your REST operation.

object GetPetRegistration extends RestRegistration {
    val method = Method.GET
    val path = "/pet/{petId}"
    val requestParams = Seq(PathParam("petId", "ID of pet that needs to be fetched"))
    def processorActor(actorSystem: ActorSystem, request: RestRequest): ActorRef = actorSystem.actorOf(Props[PetProcessor])
    override val name = "getPetById"
    override val description = "Find pet by ID"
    override val notes = "Returns a pet based on ID"
    override val errors = Seq(Error(400, "Invalid ID supplied"), Error(404, "Pet not found"))
  }

A key part of registration is defining how you return your actor using processorActor(). In the Pet Shop example REST app, we have provided 3 example scenarios:

Step 5. Using the REST Handler

In order to use RestHandler, you must first instance RestRegistry. The registry will search the code base for your RestRegistrations.

Using an Akka router, you can then instance RestHandler with the RestRegistry.

Finally, add the RestHandler to your route and start Socko web server.

The following example illustrates:

//
  // STEP #1 - Define Actors and Start Akka
  //
  val actorConfig = """
	akka {
	  event-handlers = ["akka.event.slf4j.Slf4jEventHandler"]
	  loglevel=DEBUG
	  actor {
	    deployment {
	      /rest-router {
	        router = round-robin
	        nr-of-instances = 5
	      }
	    }
	  }
	}"""

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

  val restRegistry = RestRegistry("org.mashupbots.socko.examples.rest",
    RestConfig("1.0", "http://localhost:8888/api", reportRuntimeException = ReportRuntimeException.All))

  val restRouter = actorSystem.actorOf(Props(new RestHandler(restRegistry)).withRouter(FromConfig()), "rest-router")

  //
  // STEP #2 - Define Routes
  //
  val routes = Routes({
    case HttpRequest(request) => request match {
      case PathSegments("swagger-ui" :: relativePath) => {
        // Serve the static swagger-ui content from resources
        staticContentHandlerRouter ! new StaticResourceRequest(request, relativePath.mkString("swaggerui/", "/", ""))
      }
      case PathSegments("api" :: relativePath) => {
        // REST API - just pass the request to the handler for processing
        restRouter ! request
      }
      case GET(Path("/favicon.ico")) => {
        request.response.write(HttpResponseStatus.NOT_FOUND)
      }
    }
  })

  //
  // STEP #3 - Start and Stop Socko Web Server
  //
  def main(args: Array[String]) {
    // Start web server
    val config = WebServerConfig(webLog = Some(WebLogConfig()))
    val webServer = new WebServer(config, routes, actorSystem)
    Runtime.getRuntime.addShutdownHook(new Thread {
      override def run {
        webServer.stop()
      }
    })
    webServer.start()

    System.out.println("Open your browser and navigate to http://localhost:8888/swagger-ui/index.html")
  }

Configuration

The RestConfig class is used to configure the REST handler. It is specified when instancing RestRegistry.

Required settings are:

Like other settings, you can set the values programmatically or via an external Akka configuration file. See RestConfig for more details.

Supported Data Types

The supported data types aligns with swagger.

Option, Seq or Array of the above are also supported.

Web Sockets

For a detailed discussion on how web sockets work, refer to RFC 6455.

Prior to a web socket connection being established, a web socket handshake must take place. In Socko, a WebSocketHandshakeEvent is fired when a handshake is required.

After a successful handshake, WebSocketFrameEvent is fired when a web socket text or binary frame is received.

The following route from our web socket example app illustrates:

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
      }
    
    })

Note that for a web socket handshake, you only need to call wsHandshake.authorize() in order to approve the connection. This is a security measure to make sure that web sockets can only be established at your specified end points. Dispatching to an actor is not required and not recommended.

You can also specify subprotocols and maximum frame size with authorization. If not specified, the default is no subprotocol support and a maximum frame size of 100K.

// Only support chat and superchat subprotocols and max frame size of 1000 bytes
wsHandshake.authorize("chat, superchat", 1000)

See the example web socket ChatApp for usage.

Callbacks

As part of authorize(), you are able to supply callback functions:

For both functions, a unique identifier for the web socket connection is passed as a parameter.

def onWebSocketHandshakeComplete(webSocketId: String) {
      System.out.println(s"Web Socket $webSocketId connected")
    }

    def onWebSocketClose(webSocketId: String) {
      System.out.println(s"Web Socket $webSocketId closed")
    }

    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/") => {
          // Pass in callback functions upon authorization
          wsHandshake.authorize(
            onComplete = Some(onWebSocketHandshakeComplete),
            onClose = Some(onWebSocketClose))
        }
      }
    
      case WebSocketFrame(wsFrame) => {
        // Once handshaking has taken place, we can now process frames sent from the client
        actorSystem.actorOf(Props[WebSocketHandler]) ! wsFrame
      }
    
    })

Pushing Data

After a web socket connection is authorized, it is added to the web server object’s webSocketConnections. Using this, you can push data to one or more web socket clients.

Each web socket connection has a unique identifier that you must store if you wish to push data to a specific connection. The identifier is provided in the WebSocketHandshake event as well as to the onComplete and onClose callback functions.

var myWebSocketId = ""

    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/") => {
          // Store web socket ID for future use
          myWebSocketId = wsHandshake.webSocketId     

          // Authorize
          wsHandshake.authorize()
        }
      }
        
    })

To push a message:

// Broadcast to all connections
    MyApp.webServer.webSocketConnections.writeText("Broadcast message to all web socket connections...")

    // Send to a specific connection
    MyApp.webServer.webSocketConnections.writeText("Hello", myWebSocketId)

An alternative way to push messages is to wrap the webServer instance inside an actor. You can push messages by sending the message to the actor.

class MyActor(webServer: WebServer) extends Actor {
      val log = Logging(context.system, this)
      def receive = {
        case s: String  webServer.webSocketConnections.writeText(s)
        case _       log.info("received unknown message")
      }
    }

Closing Web Socket Connections

You can check connectivity and close web socket connections:

// Close all connections
    MyApp.webServer.webSocketConnections.closeAll()

    // Close a specific web socket connection
    if (MyApp.webServer.webSocketConnections.isConnected(myWebSocketId)) {
      MyApp.webServer.webSocketConnections.close(myWebSocketId)
    }