Sockets and Lies

The Why, What, and WTF of Websockets

Me

  • I work at uStudio.
  • We make video easier to distribute.
  • We are always looking for a few good devs.

Why Websockets

When we had all finally stopped saying AJAX?

Push for Real

Websockets give us the ability to send data either way, whenever we want.

Life Before the New Hotness

            
def post(self):
    user.update(self.request.body)
    user.save(safe=True)
    # gee, I hope someone refreshes...
            
          
            
var update = function() {
  $.get("/users/foo", function(users) {
    users.forEach(function(user) {
      // compare every user to see if anything has
      // changed, and update views accordingly
    });
    setTimeout(update, 3000);
  })
};
            
          

Life After

            
def post(self):
    user.update(self.request.body)
    user.save(safe=True)
    socket.broadcast("updated_user", user.to_dict())
            
          
            
// after we've connected to the websocket...
var onmessage = function(message) {
  if (message.type == "updated_user") {
    updateUser(user.id, new_user);
  }
};
            
          

How do websockets work?

HTTP Request Primer

            
GET / HTTP/1.1
Host: www.myapp.com
Cookie: user=20a356ec01054c0b902fd265b7b1f5f4
If-None-Match: wetVrDrsf423tVSefsffv43SEf
User-Agent: Mozilla/11.0 (like Mosaic) BeOS/10.04
Accept: */*
            
          

HTTP Response Primer

            
HTTP/1.1 200 OK
Date: Thu, 24 Jan 2013 04:25:23 GMT
Content-Type: text/html; charset=utf-8
Content-Length: 18
Set-Cookie: user=20a356ec01054c0b902fd265b7b1f5f4
ETag: wetVrDrsf423tVSefsffv43SEf
Cache-Control: private, max-age=0

...ALL DAT CONTENT!
            
          

...and the request hangs up. Probably.

Websockets != HTTP

Websocket Initial Request

            
GET /websocket HTTP/1.1
Host: myapp.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://myapp.com
Sec-WebSocket-Protocol: chat, uberchat
Sec-WebSocket-Version: 13
            
          

It looks like normal HTTP, but...

  • Upgrade: websocket
  • Connection: Upgrade
  • Sec-Websocket-Key
  • Sec-Websocket-Protocol
  • Sec-Websocket-Version

Server Response

(Assuming success)

            
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat
            
          

... and stays open.

Handshake Details

  • dGhlIHNhbXBsZSBub25jZQ==
  • 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
  • hashlib.sha1(raw).digest()
  • base64.b64encode(hash_digest)

s3pPLMBiTxaQ9kYGzzhZRbK+xOo=

Upgrade Successful

...and you are powersliding down rainbows.

ws:// and wss://

  • ws://myapp.com:6550/websocket
  • wss:// uses SSL, like https://

(Simple) Messaging Overview

WebSockets send data across the wire in "frames"

Each frame conforms to a specific format

WebSocket Frame (Simplified)

...not that you'll have to implement this.

Let's See Some Code

...After More Talking

Blocking servers are bad for WebSockets.

Traditional HTTP Cycle

Alternate requests are handled in serial or by additional threads / processes

WebSocket Cycle

The server (or thread / process) can't ever release control.

So We Need Alternatives

  • Gevent - Asynchronous Monkey Patching
  • Tornado - Asynchronous via IOLoop

Gevent - "blocking" style

            
from gevent import monkey
import socket

sock = socket.socket()
sock.connect(("myapp.com", 80))
sock.send("...")
sock.recv(4096)
# will block current process until finished

monkey.patch_all()
sock = socket.socket()
sock.connect(("myapp.com", 80))
sock.send("...")
sock.recv(4096)
# releases control to other I/O operations
            
          

Tornado - "callback" style

            
from tornado.iostream import IOStream
from tornado.ioloop import IOLoop
import socket

def on_connect():
  stream.write("...")
  stream.read_bytes(4096, on_read)

def on_read(data):
  # handle data

sock = socket.socket()
stream = IOStream(s)
stream.connect(("myapp.com", 80), on_connect)
IOLoop.instance().start()
            
          

Psy - Gangnam Style

Just for reference.

Chat Demo

Right Here

Half-Duplex

We can only send one direction at a time.

Half-Duplex

            
while True:
    # we are going to wait until a message comes
    packet = websocket.receive()
    # now we can actually send a message
    websocket.send(packet)
            
          

In the blocking style, it's easier to limit yourself to half-duplex.

Twitter Demo

Right Here

Full-Duplex

            
def monitor_source():
  while True:
    # source can be twitter, redis, whatever
    message = source.receive()
    for subscriber in _SUBSCRIBERS:
      subscriber.send(message)

def monitor_subscriber(websocket):
  _SUBSCRIBERS.append(websocket)
  while True:
    message = websocket.receive()
    # perform user action (update settings, broadcast, etc.)

gevent.spawn(monitor_source)
            
          

Message Queue Architecture

Same as your database, you want a central, shared message queue.

Tornado Demo

Use the phone, Josh.

(if there is time)

Caveat Central

Shooting Unicorns in the Face

Production Notes

  • Most HTTP proxies (nginx, Apache) don't natively respect websockets.
  • You'll need TCP proxying in a production environment.
  • There is an NGINX TCP proxy module, and an Apache one.
  • These require recompiling the servers.
  • Alternatively, just use HAProxy.

Matrix of Despair

IE Chrome Firefox iOS Android
hixi-76 6 4 :( 5
hybi-07 6
hybi-10 14 7
RFC 6455 10 16 11 6

This depressing data brought to by Wikipedia.

Fallback Options

  • Flash Socket (gross)
  • EventSource
  • Long-polling
  • Forever iFrame

You Don't Have to Go It Alone

  • Socket.IO (gevent-socketio)
  • SockJS (tornado-sockjs)
  • nginx-push-stream module
  • Pusher (service)

Questions?

@joshmarshall on the Twitters.

@joshmarshall on the GitHub.