On Github nathanstilwell / socket-talk
What a WebSocket is, but mostly in the browser (window.WebSocket)
How to use the WebSocket API
Some of the concerns we've encountered making applications at Gilt
I made something to make my life easier, you might like it
Then we'll look at a really simple demo
What's a WebSocket talk without a chat demo?
A SocketWrench based client running on Github, hitting a Node-based socket server running on Heroku Play with that for a little bit, or put some questions in there, and we'll dig into the code at the end. Next week it should work better on your phone. :(A quick Google search shows there are servers written in Scala, Node, Java, Ruby, Python, C++, .NET, PHP
My favorites are Play Framework (Scala), and einaros/ws (Node.js)
These are just the ones I looked up. This used to be a problem, but no longervar cool_socket = new WebSocket('ws://coolsocket.gilt.com'); var cool_secure_socket = new WebSocket('wss://tls-socket.gilt.com');
// snip ... cool_socket.onopen = function onOpen (e) { // socket is open and ready }; cool_socket.onmessage = function onMessage (e) { // the socket has received a message }; cool_socket.onclose = function onClose (e) { // the socket has closed }; cool_socket.onerror = function onError (e) { // the socket has received an error };
// text message cool_socket.send("a utf-8 message"); // binary messages var blob = new Blob("I could have been binary data"); var arrayBuffer = new Uint8Array([1,2,3,4,5,6]); cool_socket.send(blob); cool_socket.send(arrayBuffer.buffer);
switch (cool_socket.readyState) { case WebSocket.CONNECTING : // Connecting, wait a second break; case WebSocket.OPEN : // Ready to go break; case WebSocket.CLOSING : // Shutting it down break; case WebSocket.CLOSED : // It's over break; default : // Switch statements are not my favorite }The readyState attribute represents the state of the connection. The WebSocket Object has the states build into it, so you can compare.
var cool_socket = new WebSocket('ws://coolsocket.gilt.com'); cool_socket.send('I\'m so excited I can\'t wait!'); // Bonk!
(function tryThis () { if (cool_socket.readyState === WebSocket.OPEN) { cool_socket.send('Go!'); } else { console.log('not yet'); setTimeout(tryThis, 20); } }()) // or cool_socket.onopen = function whenImOpen () { cool_socket.send('Go!'); // OK! }
In case you need to throttle
var THRESHOLD = 10240; function sendStuff (stuff) { setTimeout(function () { if (cool_socket.bufferedAmount < THRESHOLD) { cool_socket.send(stuff); } }, 1000); }
cool_socket.close(1000, "Closing normally");
socket.close(close code, "Reason")
Using WebSocket is fun and simple but when working with a mature application, be mindful of these things ...
if (window.WebSocket) { // have at it cool_socket = new WebSocket('ws://youknowthedrill.com'); } else { // rut roh, ajax fallback or go home }This seems obvious, but support is something to consider before beginning. Some applications can have an ajax fallback, some applications may not need to work in all browsers.
The WebSocket protocol allows for custom headers, but the WebSocket API does not. But we've still got the query string.
var url = 'ws://socket-server.com'; // add to the query string url += '?'; url += 'userId=nathanstilwell'; url += '&'; url += 'eventId=1834'; url += '&'; url += 'isThisGuyCool=yes'; coolsocket = new WebSocket(url);
// weak sauce cool_socket.onclose = function onClose () { cool_socket = new WebSocket('ws://socket-thing.com'); } // but pretty much the idea
At Gilt, we abstracted the "reconnect" (which is just making a new socket), and built some logic to iterate reconnection attempts.
WebSocket connections can break the same as any other network connection, so you'll need to be ready for it. The WebSocket will be kind enough to call your onclose callback, but the reconnection is up to you.cool_socket.onopen = function onOpen () { sendHeartbeat(); }; cool_socket.onclose = function onClose () { clearTimeout(heartbeatTimeout); }; function sendHeartbeat () { cool_socket.send('ping'); heartbeatTimeout = setTimeout(sendHeartbeat, 30000); }
Let the server know you're still alive
The WebSocket protocol supports ping and pong frames for keep alive, but the WebSocket API does not support client initiated ping and pongs. The browser may send pings and pongs based on its own keep-alive policy. At Gilt, we chose to implement heartbeats at a higher level, having the client send a heartbeat at an expected interval to keep track of dead connections to the server. If you connection is interrupted before the browser can send a closing handshake, your server might not realize it's missing. So we used healthchecks to shut down inactive connections.WebSocket sends messages using the MessageEvent API
The payload is in the data
cool_socket.onmessage = function onMessage (e) { e.data; // the payload }WebSocket uses the MessageEvent API, in which each message has a data attribute used for payload. At Gilt we found it necessary to have to look at each of those messages and determine if the message was to sync a clock or change a price or update inventory or sku status. To make this a little easier we added a "type" field to the data property.
WebSocket provides a single callback for all messages, so if you are sending different kinds of messages you'll have to separate them
// excerpt from web-freefall notify.subscribe('freefall/socket/message', function (msg) { if (msg.type === "PriceChange") { // *snip* } if (msg.type === "PriceChangeBootstrap") { // *snip* } if (msg.type === "Time") { // *snip* } if (msg.type === 'ViewCount') { // *snip* } });
Having built a few applications and features on WebSocket, we've solved some of the same problems a few times. And if you write something 3 times, it's probably time for an abstraction.
A light wrapper around native WebSocket with a more comfortable API
var wrench = new SocketWrench('ws://some-web-socket.com'); wrench.open(); // Open WebSocket explicitly wrench.close(); // Close WebSocket explicitly wrench.isReady(); // Check WebSocket ReadyState wrench.on(event, callback); // Add a callback to an event. returns handle wrench.off(handle); // Remove a callback from an event wrench.send(message); // Send a message to the server wrench.supported; // Check to see if WebSocket is supported. This is a Boolean
wrench.on('open', ...); // onopen wrench.on('close', ...); // onclose wrench.on('fail', ...); // onclose when you didn't close it wrench.on('error', ...); // onerror wrench.on('ready', ...); // when the readyState is OPEN
SocketWrench emits a message event when the WebSocket receives a message. But SocketWrench also parses event.data.type if it exists and will emit an event of that type.
wrench.on('message', ...); // for every message, regardless of type wrench.on('PriceChange', ...) wrench.on('PriceChangeBootstrap', ...) wrench.on('Time', ...) wrench.on('ViewCount', ...)
var wrench = new SocketWrench('ws://whatever.blah'); // messages will get buffered until your WebSocket is connected wrench.send('I'); wrench.send('don\'t'); wrench.send('care'); wrench.send('about'); // and then get sent, once the connection is ready wrench.send('ready'); wrench.send('state'); wrench.send('.'); // It will also buffer messages, during a unexpected disconnect
If the WebSocket connection is lost without explicitly calling close, SocketWrench will attempt to reconnect based on config options.
var wrench = new SocketWrench({ url: 'ws://whatever.blah', retryAttempts: 5, // try to reconnect 5 times retryTimeout: 2000 // wait 2 seconds before trying to connect again });
Retry attempts decay, so SocketWrench waits longer each time to reconnect
function decay (timeout, attempts) { return timeout * attempts; };
SocketWrench will send a text string "pong" to the server every 30 seconds to maintain the connection, and respond to a text "ping" message with a "pong". If you want this heartbeat to be something else, you can override it in the config options.
var wrench = new SocketWrench({ url: 'ws://my-socket-server.com', heartbeatInterval: 60000, // in milliseconds // could either JSON or a string heartbeatMessage : { status : 'still alive', currentMood : 'happy', outlookOnLife : 'good' } });
// node // using ws by einaros server.broadcast = function (data) { for (var i in this.clients) { this.clients[i].send(data); } }; server.on('connection', function connected (socket) { socket.send('hello'); socket.on('message', function (message) { if ('pong' !== message) { console.log('received: %s', message); server.broadcast(message); } }); });
// get jQuery and SocketWrench // // not showing lots of DOM code, Key listeners, etc // wrench = new SocketWrench({ url : 'wss://sheltered-headland-5539.herokuapp.com' }); wrench.on('chat', function onMessage (msg) { addMessage(msg.message, msg.color); scroll(); }); // gets called by key listener function transmitMessage (message) { var msg = { type: 'chat', message: message, color: randomColor }; wrench.send(msg); }
Not worrying about JSON.stringify/parse, message buffering, reconnection handling, or healthchecks