A SockJS server implementation for Play Framework.
play2-sockjs api aims to be as similar as possible to the WebSocket one provided by Play Framework:
// Play WebSocket api:
def websocket = WebSocket.using[String](handler)
// play2-sockjs api:
def sockjs = SockJS.using[String](handler)
// same request handler
val handler = { (request: RequestHeader) =>
// Log events to the console
val in = Iteratee.foreach[String](println).map { _ =>
println("Disconnected")
}
// Send a single 'Hello!' message and close
val out = Enumerator("Hello!") >>> Enumerator.eof
(in, out)
}
It provides api for Scala and Java. All transports offered by SockJS have been implemented according to the 0.3.3 protocol specifications. Currently passes all transport tests from the specs except for test_haproxy, it should impact only users that uses WebSocket Hixie-76 protocol behind HAProxy.
Current versions:
Play 2.1.x : 0.1.5
Play 2.2.x : 0.2.5
Play 2.3.x : 0.3.0
SockJS is a browser JavaScript library that provides a WebSocket-like object. SockJS gives you a coherent, cross-browser, Javascript API which creates a low latency, full duplex, cross-domain communication channel between the browser and the web server.
Under the hood SockJS tries to use native WebSockets first. If that fails it can use a variety of browser-specific transport protocols and presents them through WebSocket-like abstractions.
SockJS is intended to work for all modern browsers and in environments which don't support WebSocket protocol, for example behind restrictive corporate proxies.
Add play2-sockjs dependency to your build.sbt or project/Build.scala:
libraryDependencies <++= playVersion { v: String =>
if (v.startsWith("2.3")) Seq("com.github.fdimuccio" %% "play2-sockjs" % "0.3.0")
else if (v.startsWith("2.2")) Seq("com.github.fdimuccio" %% "play2-sockjs" % "0.2.5")
else if (v.startsWith("2.1")) Seq("com.github.fdimuccio" %% "play2-sockjs" % "0.1.5")
else Seq()
}
You may also need to add the Sonatype Repository as a resolver:
resolvers += Resolver.sonatypeRepo("releases")
or if using snapshot version:
resolvers += Resolver.sonatypeRepo("snapshots")
Since SockJS uses a complex path system to support different transports it can not be instantiated as a classic Play action handler, instead it must be used inside a SockJSRouter. Each SockJSRouter can contain only one SockJS handler, however the application can contain as many SockJSRouter as you wish.
A SockJS endpoint could be implemented in two way.
First, using SockJSRouter builder facility:
package controllers
import play.api.mvc._
import play.api.libs.iteratee._
import play.sockjs.api._
object Application extends Controller {
def index = Action {
Ok("It Works!")
}
// it must be a `val` or `lazy val` because you are instantiating a play Router and not a
// classic request handler
lazy val sockjs = SockJSRouter.using[String] { request =>
// Log events to the console
val in = Iteratee.foreach[String](println).map { _ =>
println("Disconnected")
}
// Send a single 'Hello!' message and close
val out = Enumerator("Hello SockJS!") >>> Enumerator.eof
(in, out)
}
}
and in route.conf define the route:
# Using Play sub routes include syntax `->`, map /foo url to SockJS router
-> /foo controllers.Application.sockjs
Second (more verbose), extending SockJSRouter trait:
package controllers
import play.api.mvc._
import play.api.libs.iteratee._
import play.sockjs.api._
// extends or mixin SockJSRouter trait
object SockJSController extends SockJSRouter {
// to handle a SockJS request override sockjs method
def sockjs = SockJS.using[String] { request =>
// Log events to the console
val in = Iteratee.foreach[String](println).map { _ =>
println("Disconnected")
}
// Send a single 'Hello!' message and close
val out = Enumerator("Hello SockJS!") >>> Enumerator.eof
(in, out)
}
}
and in route.conf define the path to the controller:
# Using Play sub routes include syntax `->`, map /foo url to SockJS controller
-> /foo controllers.SockJSController
and finally connect with the javascript client:
<script src="http://cdn.sockjs.org/sockjs-0.3.min.js"></script>
<script>
var sock = new SockJS('http://localhost:9000/foo');
sock.onopen = function() {
console.log('open');
};
sock.onmessage = function(e) {
console.log('message', e.data);
};
sock.onclose = function() {
console.log('close');
};
</script>
It's possible to change SockJS server side settings such as connection heartbeat or session timeout.
When using SockJSRouter builder:
package controllers
import scala.concurrent.duration._
import play.api.mvc._
import play.api.libs.iteratee._
import play.sockjs.api._
object Application extends Controller {
def index = Action {
Ok("It Works!")
}
// it's possible to change default settings
lazy val sockjs = SockJSRouter(_.websocket(false).heartbeat(55 seconds)).using[String] { request =>
...
}
// or to pass a new SockJSSettings instance
lazy val sockjs = SockJSRouter(SockJSSettings(websocket = false, heartbeat = 55 seconds)).using[String] { request =>
...
}
// or to pass a new SockJSServer instance
lazy val sockjs = SockJSRouter(SockJSServer(...)).using[String] { request =>
...
}
}
When extending SockJSRouter:
package controllers
import scala.concurrent.duration._
import play.api.mvc._
import play.sockjs.api._
// mixin SockJSRouter trait with your controller
object SockJSController extends Controller with SockJSRouter {
// override this method to specify a different SockJSServer instance with custom settings
override val server = SockJSServer(SockJSSettings(websocket = false, heartbeat = 55 seconds)
// here goes the request handler
def sockjs = SockJS.using[String] { request =>
...
}
}
Note: each SockJSRouter will have is own SockJSServer
Here is a short example of how to implement a SockJS endpoint in Java (for Java8 see below):
package controllers;
import play.libs.F;
import play.mvc.*;
import play.sockjs.*;
public class Application extends Controller {
public static SockJSRouter hello = new SockJSRouter() {
// override sockjs method
public SockJS sockjs() {
return new SockJS() {
// Called when the SockJS Handshake is done.
public void onReady(SockJS.In in, SockJS.Out out) {
// For each event received on the socket,
in.onMessage(new Callback<String>() {
public void invoke(String event) {
// Log events to the console
System.out.println(event);
}
});
// When SockJS connection is closed.
in.onClose(new Callback0() {
public void invoke() {
System.out.println("Disconnected");
}
});
// Send a single 'Hello!' message
out.write("Hello!");
}
};
}
}
}
in route.conf define the route:
# Using Play sub routes include syntax `->`, map /foo url to SockJS router
-> /hello controllers.Application.hello
To configure it you can use @SockJS.Settings annotation:
package controllers;
import play.libs.F;
import play.mvc.*;
import play.sockjs.*;
public class Application extends Controller {
public static SockJSRouter hello = new SockJSRouter() {
@SockJS.Settings(
cookies = CookieCalculator.JSESSIONID.class,
websocket = false,
heartbeat = 55000 // duration in milliseconds
)
public SockJS sockjs() {
return new SockJS() {...}
}
}
}
If you are using Java 8 you can take advantage of Lambda Expressions (api contribution by Ariel Scarpinelli):
package controllers;
import play.mvc.*;
import play.sockjs.*;
public class Application extends Controller {
// SockJS endpoint handler with default configuration:
public static SockJSRouter hello = SockJSRouter.whenReady((in, out) -> {
// Log each event received on the socket to the console
in.onMessage(System.out::println);
// When SockJS connection is closed.
in.onClose(() -> System.out.println("Disconnected"));
// Send a single 'Hello!' message
out.write("Hello!");
});
// and if default configuration isn't enough:
public static SockJSRouter helloNoWebSocket = SockJSRouter.withWebSocket(false).whenReady((in, out) -> {
...
});
}
If your Play application is deployed in a load balanced environment you must make sure that all requests for a single session must reach the same server.
SockJS has two mechanisms that can be useful to achieve that:
* Urls are prefixed with server and session id numbers, like:
/resource/<server_number>/<session_id>/transport. This is useful for load
balancers that support prefix-based affinity (HAProxy does).
* JESSIONID cookie: it's possible to enable cookie writing for load balancers that
support sticky sessions. In order to enable this feature please supply
SockJSSettings.CookieCalculator.jessionid when configuring SockJSServer, it's disabled
by default. It's also possible to implement custom CookieCalculator.
In the samples/ folder there are two sample applications:
* sockjs-chat: a port of the Play sample websocket-chat to SockJS
* sockjs-protocol-test: server for SockJS 0.3.3 protocol specifications tests
Currently, on 68 tests only 3 do not pass. As mentioned, the most important is test_haproxy. The other two failing tests are edge cases of test_abort_xhr_streaming and test_abort_xhr_polling due to different implementation details, however session closure is handled correctly.