27 February 2012
Intermediate
The WebSocket API is one of the more powerful new features in the HTML5 specification because it opens the door to real-time communication and pushing messages.
This article describes a basic chat program that shows the basics of WebSockets and how to implement them on the client side.
The WebSocket API has been somewhat volatile over the past year as the W3C specification has been solidified. It has finally been completed and the specification can now be implemented consistently across browsers.
Instead of using the HTTP protocol, WebSockets use their own protocol. There is a significant amount of overhead incurred whenever communication over HTTP happens. Because of the request/response mechanism and all of the information that HTTP stores in header information, exchanging even basic information can result in lots of data being sent back and forth.
WebSockets, by contrast, are full duplex, which means they can communicate back and forth at the same time without the request/response overhead. The header information is also much smaller, so the bulk of the data being exchanged is the actual data from the application.
Most of the major browsers now support some version of WebSockets. Firefox, Chrome, and the latest version of Internet Explorer all have added support for the WebSocket API. Safari and Opera offer partial support for the API.
One of the major issues is understanding which draft of the WebSocket spec is supported by the browsers. Wikipedia has a good entry that lists the specifications by version number and which browser versions support them; for details, visit http://en.wikipedia.org/wiki/WebSocket#Browser_support. Going forward, the final version of the specification, RFC 6455, is the one that will be implemented.
When working with WebSockets, you need to have a server that supports them. Complete instructions for configuring a server that adheres to the WebSockets specification are beyond the scope of this article, but it's an important enough topic to address at least briefly. There are a few different ways to potentially implement WebSockets.
PHP ships with built-in support for WebSockets, so you could write your own PHP socket server that handles the requests and responses from the client code. There are also Java and Ruby projects that provide WebSocket support for those languages.
One of the more interesting ways to get up and running is a project called Socket.io that runs on Node.js. It has server-side and client-side libraries that make using WebSockets very easy. Node.js lets you use JavaScript on the server so the client- and server-side languages can be the same. For basic socket testing, websocket.org hosts a test server at http://websocket.org/echo.html that will simply send the transmitted data as a response back to the client.
The server I use comes from Kevin Hoyt who wrote a socket server using Adobe AIR. For details, see the AIRWebSocket project on Github.
The core of the WebSocket API is the WebSocket class, which provides the methods and events that handle all of the communication with the server.
It is important to have your code first check that the browser supports WebSockets. The quickest way to do this is to see if window.WebSocket exists. A more powerful solution is to use the Modernizr library, which helps detect support for WebSockets while providing a graceful fallback for older browsers.
The example chat application provides a Connect button that the user can use to initiate the connection to the socket server. This process is implemented in a connect() function:
var connection = {};
function connect() {
if(window.WebSocket != undefined) {
if(connection.readyState === undefined || connection.readyState > 1)
{
connection = new WebSocket('ws://localhost:1740');
}
}
}
The first line of code above defines the connection object that will be used by the rest of the application. When you make it a global variable, the connection object can be used in other functions. After checking to make sure that the browser supports WebSockets, the code checks to make sure there isn't already a connection active.
The WebSocket object provides a readyState property that indicates the connection's ready status. The values are as follows:
const unsigned short CONNECTING = 0;
const unsigned short OPEN = 1;
const unsigned short CLOSING = 2;
const unsigned short CLOSED = 3;
As long as the readyState is greater than 1, the connection isn't open so the application can connect to the socket server.
Connecting to the server is just a matter of instantiating the WebSocket class and passing in the URL and port number of the socket server. The browser then makes a connection with the server.
The WebKit browsers and Opera handle WebSockets in the same way, but in Firefox the WebSocket object has a prefix; it is referred to as MozWebSocket . Beyond that, the APIs are the same, so an easy way to keep everything simple is to check for the existence of window.MozWebSocket and then set it to the regular WebSocket object.
if (window.MozWebSocket) {
window.WebSocket = window.MozWebSocket;
}
If the connection is successful, the browser will fire an open event. To make sure this gets caught, the WebSocket API includes an onopen property, which is assigned to a function that will run code for every open event.
The code below sets the onopen property to a corresponding onopen() function that sets a couple of variables so that the UI is updated to indicate that the user is logged in.
connection.onopen = onopen;
Here is the onopen() function:
function onopen (event) {
document.getElementById('connected').innerHTML = "Connected";
document.getElementById('chat').innerHTML = "You have joined the chat<br />";
}
Note that most people don't actually assign those methods to named functions but rather include them in anonymous functions right where they are first defined. I have implemented it this way because I like having the separation, but it may seem a bit redundant to you as you dig into more WebSocket examples.
Now that your client is connected you can start dealing with actual chat messages. The server I have set up for the moment just cycles through all of the currently connected users whenever it gets a message and then sends that message out to all of those users. Though it is a pretty basic chat server, it illustrates many key WebSocket concepts.
With WebSockets you can send text, or UTF-8 data, as well as binary data such as pictures or videos. They both use the same API on the client side, but it will largely depend on the server to actually handle the data types correctly.
To send a message to the socket server the chat application simply invokes the send() method of the connection object. It takes a single parameter, the message being sent, which it passes to the socket server.
When the user clicks on the Send Message button in the chat application, the sendmessage() method sends the message typed by the user along with the username. The socket server will then loop through all the clients, including the sender, and deliver the message to them.
function sendmessage() {
var messagetext = document.getElementById('chatmessage').value;
messagetext = username + ": " + messagetext;
connection.send(messagetext);
}
To handle incoming messages, the WebSocket API uses the onmessage property of the connection event. Just like the onopen property covered earlier, this property takes a function that will be called whenever a new message arrives. So the first step is to set up the event handler in the original connect() method right before the onopen definition:
connection.onmessage = onmessage;
Once that is set up, define the onmessage() function:
function onmessage (event) {
var chatdiv = document.getElementById('chat');
chatdiv.innerHTML = chatdiv.innerHTML + event.data + "<br />";
}
The event that is received by onmessage is of type MessageEvent . It includes a data property that has the value of the message being received. In this case, that is used to display the chat text. This data property includes the username of the chat participant along with the message they sent. This value is appended to the chat div window.
Error handling is an important topic to cover, even if only quickly. Along with onopen and onmessage, the WebSocket API also includes an onerror property, which takes a function that runs any time an error occurs. The error event includes a data property that provides some information about the error.
Here is the basic error handler used in the chat application:
function onerror(event) {
console.log(event);
document.getElementById('chat').innerHTML = "There was an error: " + event.data;
}
By enabling the exchange of real-time data, the WebSocket API opens some interesting possibilities when combined with the rest of the host of new HTML5, JavaScript, and CSS3 features.
One of the cooler demos I've seen is a collaborative whiteboard using the canvas element. Every time someone connected to the socket draws on it, a message gets sent out to the connected clients so everyone can see what is being drawn. It's a neat idea, but all that's really happening under the hood is that the socket server and application are exchanging a set of x and y coordinates in text.
To illustrate the binary capabilities of the WebSocket API, I implemented a similar application that uses binary data. Specifically, I implemented a quick canvas painting feature that the user can use to draw something on a small canvas area. When the user clicks a button, the application does not send a set of coordinates to the socket server, but rather takes a snapshot of the image and sends it as binary data to the socket server. The socket server sends the data back as an image, which will appear in the chat window of all connected clients. This demo will only work in the latest version of Chrome because binary WebSocket support is still somewhat on the cutting edge.
To send and receive binary data correctly you need to set up a binaryType for the WebSocket API. The binaryType can be either arraybuffer or blob , which are the two basic binary types that JavaScript supports. You can use either one depending on what you're sending and how you want to access it. I found arraybuffer to be ideal for this example because it's easy to iterate through an array, and I found that I had to copy a lot of data back and forth between arrays. So the WebSocket setup code becomes this:
connection = new WebSocket('ws://localhost:1740');
connection.binaryType = "arraybuffer";
connection.onopen = onopen;
connection.onmessage = onmessage;
connection.onclose = onclose;
connection.onerror = onerror;
Now you need to get binary data out of the canvas. I wrote a sendphoto() method that does the work of pulling the binary data out of the canvas element on the page. It uses the getImageData() method to get the actual binary array data and then it loops through the data and inserts it into a Uint8Array. The code accesses the buffer property of this array and sends it using the WebSocket API.
function sendphoto() {
imagedata = context.getImageData(0, 0, imagewidth,imageheight);
var canvaspixelarray = imagedata.data;
var canvaspixellen = canvaspixelarray.length;
var bytearray = new Uint8Array(canvaspixellen);
for (var i=0;i<canvaspixellen;++i) {
bytearray[i] = canvaspixelarray[i];
}
connection.send(bytearray.buffer);
context.fillStyle = '#ffffff';
context.fillRect(0, 0, imagewidth,imageheight);
}
That data goes to the socket server and the socket server sends the binary data back out to all of the connected clients. If you're interested in seeing how the server does that, you can take a look at the Github project for the code.
To handle incoming binary messages, you'll need to modify the onmessage() function. Because you'll have to handle two types of data, the ArrayBuffer and the String data, you'll want to check the instanceof property of event.data and route the data accordingly.
Once you do that, the process will be to translate the ArrayBuffer data into a typed JavaScript array. Then, create a temporary Canvas element that is used to insert the ArrayBuffer data by manipulating the image data of the canvas. Finally, with the image stored in the temporary canvas, use the toDataURL() method to get a URL string that you can set as the source of an img element, which then gets displayed on the screen.
if(event.data instanceof ArrayBuffer)
{
var bytearray = new Uint8Array(event.data);
var tempcanvas = document.createElement('canvas');
tempcanvas.height = imageheight;
tempcanvas.width = imagewidth;
var tempcontext = tempcanvas.getContext('2d');
var imgdata = tempcontext.getImageData(0,0,imagewidth,imageheight);
var imgdatalen = imgdata.data.length;
for(var i=8;i<imgdatalen;i++)
{
imgdata.data[i] = bytearray[i];
}
tempcontext.putImageData(imgdata,0,0);
var img = document.createElement('img');
img.height = imageheight;
img.width = imagewidth;
img.src = tempcanvas.toDataURL();
chatdiv.appendChild(img);
chatdiv.innerHTML = chatdiv.innerHTML + "<br />";
}
And with that, you're sending and receiving text and binary messages with the WebSocket API.
This tutorial provided an introduction to the WebSocket API and how to use it. Even though the API itself is pretty straightforward, there are a surprising number of great uses for it. Everything from basic chat to real-time games or enterprise dashboards that need real-time data can all rely on the WebSocket API for their communications.
Mozilla's Developer Network has some great content on WebSockets that applies to both WebKit and Firefox; visit https://developer.mozilla.org/en/WebSockets for details. Also take a look at Socket.io, which is great way to get started with WebSockets without having to write much code on the server to make the connections happen.
Explore the sample files for this article for the client-side source code. You'll need both it and the AIR-based socket server from Github to get the application to work. The client-side code by itself should give you a good idea of how to use the WebSocket API for any socket server that supports binary data.
This work is licensed under a Creative Commons Attribution-Noncommercial-Share Alike 3.0 Unported License. Permissions beyond the scope of this license, pertaining to the examples of code included within this work are available at Adobe.