Hookup by BackChat.io

Reliable messaging over websockets

Hookup Servers

At this stage creating servers is only really supported in scala. To work with servers there are a few concepts that need a bit more explanation. We'll start with configuration of the server before moving on to what the server can do.

Configuring the server.

At this stage the server is implemented with Netty and as such you can configure it to work with other netty modules. We don't have an abstraction on top of Netty's request and response model and as such you can use it with any other server that is implemented with Netty channel handlers.

To start a server with all the defaults you can use the following code:

val server = HookupServer(8125) {
  new HookupServerClient {
    def receive = {
      case TextMessage(text) 
        println(text)
        send(text)
    }
  }
}

server onStop {
  println("Server is stopped")
}
server onStart {
  println("Server is started")
}
server.start
Creating a server

A HookupServer has 2 methods that allow you to register callbacks on either startup or shutdown. There are more things you can configure on a server than just the port. Take a look at the ServerInfo class to find out all the properties.

Liquid error: undefined method `[]' for nil:NilClass

You can also configure the server from a typesafe config object, a full config looks something like this.

myServer {
  listenOn = "0.0.0.0"
  port = 8765
  pingTimeout = "50 seconds"
  contentCompression = 6
  subProtocols = ["jsonProtocol", "simpleJson"]
  ssl {
    keystore = "./ssl/keystore.jks"
    password = "changeme"
    algorithm = "SunX509"
  }
  flashPolicy {
    domain = "*.example.com"
    ports = [80, 443, 8080, 8843]
  }
  maxFrameSize = "32kb"
}

Some of the properties in this config will become more clear in the rest of this document.

To configure the modules that make up the default Hookup server we need to talk about ServerCapabilities. A server capability is available for you to extend yourself with your own modules if you want to use that form of configuration. If server capabilities don't allow the flexibillity you want then you can always subclass a HookupServer and make use of the template methods it provides. Let's start with the Ping capability.

Ping configuration

You can use the Ping configuration to configure whether the server should send out pings to clients when the connection has been idle for a while. The websocket protocol prescribes that the client is the one sending the pings but your use case may vary, you may need to appease a proxy in between. You configure the ping configuration with a Timeout.

implicit val jsonFormats: Formats = DefaultFormats
implicit val wireFormat: WireFormat = new JsonProtocolWireFormat

HookupServer(Ping(Timeout(2 minutes))) {
  new HookupServerClient {
    def receive = { case _ =>}
  }
}

You can also configure compression for a server.

Compression configuration

By default the server doesn't use any content compression, but you can configure it to do so by adding the ContentCompression capability. By default this capability is configured to a compression level of 6, this can of course be changed

HookupServer(ContentCompression(2)) {
  new HookupServerClient {
    def receive = { case _ =>}
  }
}

You can also configure the max frame size of a server.

Max frame size

Netty requires you to set a maximum frame length for a websocket frame, by default it uses Long.MaxValue as max framesize, you can configure that to any number of bytes. The capability you need for this is the MaxFrameSize.

HookupServer(MaxFrameSize(512*1024)) {
  new HookupServerClient {
    def receive = { case _ =>}
  }
}

If you need SSL support for your server, basic support is included.

SSL configuration

For basic SSL support you can use the SslSupport capability. It uses the default truststore for your system but allows you to configure the keystore. The default values are provided through the standard java system properties: -Dkeystore.file.path=..., -Dkeystore.file.password=..., -Dssl.KeyManagerFactory.algorithm=SunX509.

val sslSupport =
  SslSupport(
    keystorePath = "./ssl/keystore.jks",
    keystorePassword = "changeme",
    algorithm = "SunX509")

HookupServer(sslSupport) {
  new HookupServerClient {
    def receive = { case _ =>}
  }
}

The hookup server also has support for subprotocols.

Subprotocol configuration

The WebSocket spec provides a section on subprotocols, you can provide a number of [[WireFormat]] registrations with a name and those will be used to negotiate the wire format that will be used for this connection. This allows for 1 server to server many protocols and the protocol to talk to the server can be chosen by the client.

// these wire formats aren't actually implemented it's just to show the idea
HookupServer(SubProtocols(new NoopWireformat("irc"), new NoopWireformat("xmpp"))) {
  new HookupServerClient {
    def receive = { case _ =>}
  }
}

By default the server is configured with:

val SimpleJson = new SimpleJsonWireFormat()(DefaultFormats)
val JsonProtocol = new JsonProtocolWireFormat()(DefaultFormats)
/**
 * The default protocols hookup understands
 */
@BeanProperty
val DefaultProtocols = Seq(SimpleJson, JsonProtocol)

If no defaultProtocol name has been specified it will use the simpleJson protocol which just parses json messages or text messages, but doesn't add any reliability features. To enable the reliability features by default you have to set the defaultProtocol property. The default protocols are always available but you can add your own.

The last of the default capabilities is the flash policy server.

Flash policy configuration

In certain browsers you may need to resort to using a flash implementation of the websocket. To allow socket connections to your server flash requires you to run a flash policy server. We let it fallback to the port that serves the websocket. Leaving this configuration as default will allow all flash connections.

val server = HookupServer(port, FlashPolicy("*.example.com", Seq(80, 443, 8080, 8843, port))) {
  new HookupServerClient {
    def receive = { case _ =>}
  }
}

This is the entire rundown of creating and configuring a server, when a server is started it's probably going to accept connections.

Accepting connections

When a client connects, the server creates a HookupServerClient. You provide the factory when you create the server by creating a new instance of an implementation of a HookupServerClient.

The HookupServerClient allows you to keep state per connection, but you have to be aware that the client will potentially be accessed by multiple threads so you have to take care of synchronization (or use an actor for example).

Sending messages

Once connected a HookupServerClient can send messages to the current connection or broadcast a message to all connected clients. When you send a message you get an akka Future back, you can cancel this listen for an operation result. The operation result exists to work with many operations at the same time like in a broadcast you will receive failure and success status for every client connection you broadcasted to.
There are 3 operation results: Success, Cancelled and ResultList, the error case is handled in the onFailure method of the future.

A HookupServerClient provides an alias for send as ! and for broadcast as ><.

Receiving messages

A HookupServerClient requires you to implement one method or val a Hookup.Receive partial function that handles InboundMessage implementations and returns Unit. Below there's an example of a server client that prints all events it receives and echoes message events back.

new HookupServerClient {
  def receive = {
    case Connected 
      println("client connected")
    case Disconnected(_) 
      println("client disconnected")
    case m @ Error(exOpt) 
      System.err.println("Received an error: " + m)
      exOpt foreach { _.printStackTrace(System.err) }
    case m: TextMessage 
      println(m)
      send(m)
    case m: JsonMessage 
      println("JsonMessage(" + pretty(render(m.content)) + ")")
      send(m)
  }
}