BackChat.io Hookup
Reliable messaging over websockets with akka.
View the Project on GitHub backchatio/hookup
View the Server guide server guide
This project is maintained by BackChat.io, the real-time data filtering API for the cloud.
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)
}
}