Dojo has an API for Comet-style real-time communication based on the WebSocket API. WebSocket provides a bi-directional connection to servers that is ideal for pushing messages from a server to a client in real-time. Dojo’s dojox/socket
module provides access to this API with automated fallback to HTTP-based long-polling for browsers (or servers) that do not support the new WebSocket API. This allows you start using this API with Dojo now.
The dojox/socket
module is designed to be simple, lightweight, and protocol agnostic. In the past Dojo has provided protocol specific modules like CometD and RestChannels, but there are numerous other Comet protocols out there, and dojox/socket
provides the flexibility to work with virtually any of them, with a simple foundational interface. The dojox/socket
module simply passes strings over the HTTP or WebSocket connection, making it compatible with any system.
The simplest way to start a dojox/socket
is to simply call it with a URL path:
require(["dojox/socket"], function (Socket) {
// Create socket instance
var socket = new Socket("/comet");
});
require(["dojox/socket"], function (Socket) {
// Create socket instance
var socket = new Socket("/comet");
});
The socket module will then connect to the origin server using WebSocket, or HTTP as a fallback. We can now listen for message events from the server:
socket.on("message", function(event){
var data = event.data;
// do something with the data from the server
});
Here we use the socket.on()
event registration method (inspired by socket.io and NodeJS’s registration method) to listen to “message events” and retrieve data when they occur. This method is also aliased to the deprecated Dojo style socket.connect()
.
We can also use send()
to send data to the server. If you have just started the connection, you should wait for the open
event to ensure the connection is ready to send data:
socket.on("open", function(event){
socket.send("hi server");
});
Finally, we can listen for the connection being closed by the server or network by listening for the close
event. And we can initiate the close of a connection from the client by calling socket.close()
.
The dojox/socket
method can also be called with standard Dojo IO arguments to initiate the communication with the server. This makes it easy to provide any necessary headers for the requests. For example:
var socket = new Socket({
url:"/comet",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
}});
This will automatically translate the relative URL path to a WebSocket URL (using ws://
scheme) or an HTTP URL depending on the browser capability.
For some applications, the server may only support HTTP/long-polling (without real WebSocket support). We can also explicitly create a long-poll based connection:
var socket = new Socket.LongPoll({
url:"/comet",
headers: {
"Accept": "application/json",
"Content-Type": "application/json"
}});
We can also provide alternate transports in the socket arguments object. This would allow us to use the get()
method in dojo/io/script
to connect to a server. However, a more robust solution is to use the dojox/io/xhrPlugins for cross-domain long-polling, which will work properly with dojox/socket
.
Auto-Reconnect
In addition to dojox/socket
, we have also added a dojox/socket/Reconnect
module. This wraps a socket, adding auto-reconnection support. When a socket is closed by network or server problems, this module will automatically attempt to reconnect to the server on a periodic basis, with a back-off algorithm to minimize resource consumption. We can upgrade a socket to auto-reconnect by this simple code fragment:
require(["dojox/socket", "dojox/socket/Reconnect"],
function (Socket, Reconnect) {
// Create socket instance
var socket = new Reconnect(new Socket("/comet"));
});
Using Dojo WebSocket with Object Stores
One of the other big enhancements in Dojo is the Dojo object store API (which supercedes the Dojo Data API), based on the HTML5 IndexedDB object store API. Dojo comes with several store wrappers, and the Observable
store provides notification events that work very well with Comet driven updates. Observable
is a store wrapper. To use it, we first create a store, and then wrap it with Observable:
require([
"dojo/store/JsonRest",
"dojo/store/Observable"
], function(JsonRest, Observable){
var store = Observable(new JsonRest({data:myData}));
});
This store will now provide an observe()
method on query results that widgets can use to react to changes in the data. We can notify the store of changes from the server by calling the notify()
method on the store:
socket.on("message", function(event){
var existingId = event.data.id;
var object = event.data.object;
store.notify(object, existingId);
});
We can signal a new object by calling store.notify()
and omitting the id, and a deleted object by omitting the object (undefined). A changed/updated object should include both.
Handling Long-Polling from your Server
Long-polling style connection emulation can require some care on the server-side. For many applications, the server may have sufficient information from request cookies (or other ambient data) to determine what messages to send the browser. However, other applications may vary on what information should be sent to the browser during the life of the application. Different topics may be subscribed to and unsubscribed from. In these situations, the server may need to correlate different HTTP requests with a single connection and its associated state. While there are numerous protocols, one could do this very easily be defining a unique connection and adding that as a header for the socket (the headers are added to each request in the long-poll cycles). For example, we could do:
require([
"dojox/socket"
], function(Socket){
var socket = Socket.LongPoll({
url:"/comet",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Client-Id": Math.random()
}});
});
In addition, dojox/socket
includes a Pragma: long-poll
to indicate the first request in a series of long-poll requests to help a server ensure that the connection setup and timeout is properly handled.
We can easily use dojox/socket
with other protocols as well:
CometD
To initiate a Comet connection with a CometD server, we can do a CometD handshake, connection, and subscription:
var socket = new Socket("/cometd");
function send(data){
return socket.send(json.stringify(data));
}
socket.on("connect", function(){
// send a handshake
send([
{
"channel": "/meta/handshake",
"version": "1.0",
"minimumVersion": "1.0beta",
"supportedConnectionTypes": ["long-polling"] // or ["callback-polling"] for x-domain
}
])
socket.on("message", function(data){
// wait for the response so we can connect with the provided client id
data = json.parse(data);
if(data.error){
throw new Error(error);
}
// get the client id for all future messages
clientId = data.clientId;
// send a connect message
send([
{
"channel": "/meta/connect",
"clientId": clientId,
"connectionType": "long-polling"
},
{ // also send a subscription message
"channel": "/meta/subscribe",
"clientId": clientId,
"subscription": "/foo/**"
}
]);
socket.on("message", function(){
// handle messages from the server
});
});
});
Socket.IO
Socket.IO provides a lower-level interface like dojox/socket
, providing simple text-based message passing. Here is an example of how to connect to a Socket.IO server:
require([
"dojo/request", "dojox/socket"
], function(request, Socket){
var
args = {},
ws = typeof WebSocket != "undefined",
url = ws ? "/socket.io/websocket" : "/socket.io/xhr-polling";
var socket = new Socket(args = {
url:url,
headers:{
"Content-Type":"application/x-www-urlencoded"
},
transport: function(args, message){
args.data = message; // use URL-encoding to send the message instead of a raw body
request.post(url, args);
}
});
var sessionId;
socket.on("message", function(){
if (!sessionId){
sessionId = message;
url += '/' + sessionId;
}else if(message.substr(0, 3) == '~h~'){
// a heartbeat
}
});
});
Comet Session Protocol
And here is an example of connecting to a Comet Session Protocol server (the following example was tested with Orbited, but could work with Hookbox, APE, and others):
require([
"dojo/json", "dojox/socket"
], function(json, Socket){
var args, socket = new Socket(args = {
url: "/csp/handshake"
});
function send(data){
return socket.send(json.stringify(data));
}
var sessionId = Math.random().toString().substring(2);
socket.on("connect", function(){
send({session:sessionId});
socket.on("message", function(){
args.url = "/csp/comet";
send({session:sessionId});
});
});
});
Tunguska
Tunguska provides a Comet-based interface for subscribing to data changes. This is a very simple protocol which allows us to communicate with a Tunguska server:
var socket = new Socket({
url:"/comet",
headers: {
"Accept": "application/json",
"Content-Type": "application/json",
"Client-Id": Math.random()
}});
function send(data){
return socket.send(json.stringify(data));
}
socket.on("connect", function(){
// now subscribe to all changes for MyTable
send([{"to":"/MyTable/*", "method":"subscribe"}]);
});
Conclusion
Dojo’s socket API is a flexible simple module for connecting to a variety of servers and building powerful, efficient real-time applications without constraints. This adds to the array of awesome features in Dojo.