socket-talk



socket-talk

0 0


socket-talk

A socket wrench talk for the Full Stack meetup

On Github nathanstilwell / socket-talk

Using the WebSocket API @ Gilt

Nathan Stilwell

Nathan Stilwell

(Front-end) Software Engineer @ Gilt

github.com/nathanstilwell

@nathanstilwell

Introduce yourself I don't really use Twitter

What are we doing?

  • What's a WebSocket?
  • How do you use it?
  • Some stuff you'll deal with,
  •     unless you use SocketWrench (I wrote it!)
  • Then I prove how easy this all is (Demo Code!)

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

Trivial Demo!

bit.ly/stupid-chat

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. :(

What is WebSocket?

  • "a protocol providing full-duplex communications channels over a single TCP connection."
  • International Engineering Task Force standard, RFC-6455
  • API standardized by the W3C If youre here tonight, you probably care about the API.

Why is it Awesome?

  • full-duplex, genuine real-time communication over one connection
  • Less bandwidth
  • Standard (mostly) 1. HTTP vs WebSocket (Walkie Talkie vs telephone), browser and server are peers 2. HTTP headers vs 2 byte overhead 3. Backed by the W3C instead of some 3rd party solution like Flash or Comet

Browser Support

caniuse.com/#search=websocket

  • IE 10+
  • Firefox 11+
  • Chrome 14+
  • Safari 6+
  • Opera 12.1+
  • iOS Safari 6+
  • Android Browser 4.4+
  • Opera Mobile 12.1+
  • Blackberry 7.0+
  • Chrome for Android 32+
  • Firefox for Android 26+
  • IE Mobile 10+ Modern Browsers, so still consider fallbacks for features that need to working oldIE

Servers?

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 longer

How Gilt met WebSocket

2011 - New Initiative "Customer" Excitement Show actvity on the site We wanted to play with some new toys

Gilt Live

we tried out the Play framework because it made websockets really easy It listens to a service and sends socket messages to clients, that create Backbone Views. It also sends inventory updates so users can see what is selling out. It was a really fun experiment

Freefall Sales

An experimental sale type that last 5 minutes and during that time, we drop the price by up to 70% We used WebSocket to communicate the price dropping for everyone in the sale at the same time, and since inventory was limited, we needed acurate purchase times. Also we used inventory updates.

Inventory Updates

Having used it in the past two apps, we moved the functionality to the listing page

SKU Attribute Updates

We rewrote the Product Detail Page last year, and since we were on a WebSocket roll we decided to listen to sku attribute updates and reflect live changes

Let's do this!

The API is actually really simple. Like really simple.

new WebSocket

  var cool_socket = new WebSocket('ws://coolsocket.gilt.com');
  var cool_secure_socket = new WebSocket('wss://tls-socket.gilt.com');

Hook up some callbacks

  // 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
  };

Send messages to the server

  // 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);

the ReadyState

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.

Check it before you wreck it

  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!
  }

bufferedAmount

In case you need to throttle

  var THRESHOLD = 10240;
  function sendStuff (stuff) {
    setTimeout(function () {
      if (cool_socket.bufferedAmount < THRESHOLD) {
        cool_socket.send(stuff);
      }
    }, 1000);
  }

Shut it down

  cool_socket.close(1000, "Closing normally");

socket.close(close code, "Reason")

Some things to consider

Using WebSocket is fun and simple but when working with a mature application, be mindful of these things ...

Test for WebSocket support

  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.

Use the opening handshake for setup

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);

Failed connections, how to reconnect?

// 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.

Healthchecks to monitor your connections

  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.

Parsing message "types"

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.

Parsing message "types" (cont.)

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*
  }
});

Paving the cow path

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.

SocketWrench

https://github.com/nathanstilwell/socket-wrench

A light wrapper around native WebSocket with a more comfortable API

The F's and B's

  • Event emitter API
  • Explicit open and close
  • Message buffering while not connected
  • Reconnection handling
  • Healthcheck and custom heartbeats
  • Connection Data (pass data on the query string of the opening handshake)
  • JSON parsing

SocketWrench 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

SocketWrench.on()

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.on('message', [callback])

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', ...)

Buffering messages while you're not connected

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

Reconnection handling

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;
};

Heartbeats / Pings / Pongs

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'
  }
});

SocketWrench still experimental

  • No binary data support
  • Terrible test coverage
  • Known bugs

Try it out, file some bugs, use it as a reference.

Let's tear apart that trivial demo

bit.ly/stupid-chat-code

Crack open Sublime Text and reveal how stupidly simple it is.

The Server

// 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);
    }
  });
});

The Client

// 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

Learn more

Thanks for listening

bit.ly/socket-talk // just in case

Slides available at tech.gilt.com