Real-time Web Application with Socket.IO, Node.js, and Redis

York Tsai, JSDC 2013

Who is York

VP Engineering @ EZTABLE

Never Live DEMO!!

http://jsdc.york.tw/

I hope this work on my (and yours) computer ...

In This DEMO




You may Have Heard About

  • Forever Iframe
  • XMLHttpRequest Long Polling
  • Cometd
  • Websocket
http://en.wikipedia.org/wiki/Comet_(programming)

TL;DR

Socket.IO

  • Cross browser (including IE 6)
  • Real-time and bi-directional persistent connection (WebSocket)
  • Very simple to use
  • Javascript!

Start a Server


express = require("express")
app = express()
server = require("http").createServer(app)
io = require("socket.io").listen(server)

server.listen 80
                        

Sending and Receiving Events

Server


io.sockets.on "connection", (socket) ->
    socket.emit "event_X",
        hello: "world"

    socket.on "event_Y", (data) ->
        # will get {my: "data"}
                        

Client


socket = io.connect("http://localhost")
socket.on "event_X", (data) ->
    # data is {hello: "world"}
    socket.emit "event_Y",
        my: "data"
                        

Select targets for your event

Broadcasting


io.sockets.on "connection", (socket) ->
    socket.broadcast.emit "myevent" "a new user connected"
                        

Rooms

Join / Leave


socket.join('room')
socket.leave('room')
                        

Emitting


io.sockets.in('room').emit('event_name', data)
                        

Single Target


clients = []
io.sockets.on('connection', (socket) ->
    clients.push(socket.id)
)

io.sockets.socket(clients[0]).emit "for_first_one", 
    msg: "You are the first!"
                        

Multiple applications?

Namespacing

Server


io.of("/my_app")
  .on("connection", (socket) ->
    # event handlers
)
                        

Client


myAppClient = io.connect("http://localhost/my_app")
                        

Get old data for new connections?

Data Persistence

  • Need a persistence layer.
  • Get old data from the persistence layer whenever a new connection established

Integration

I have component X,Y,Z want to send/receive data from user's browser

Pub/Sub or Message Queue

Messaging system usually supports both

Pub/Sub


  • Super-fast in-memory data structure server
  • Pub/Sub
  • Rich client libraries
  • Zero install and easy to operate

Only 3 Commands

  • SUBSCRIBE channel
  • UNSUBSCRIBE channel
  • PUBLISH channel message

In addition...

You can connect multiple node.js server to one redis channel

Scalability!

Two Connections per Server

Redis-backed Pub/Sub

  • Two clients required: one for pub, one for sub
  • Use one channel, add metadata to your message

Redis-backed Pub/Sub


redisSubClient.on "ready", ->
    redisSubClient.subscribe redisChannel

redisSubClient.on "message", (channel, data) ->
    data = JSON.parse(data)
    ioEvent = data.event
    ioData = data.data

    io.sockets.in(data.channel)
              .emit(ioEvent, ioData)
                        

Frequently Asked Questions

SSL?

Yes!

SSL Server


privateKey = fs.readFileSync("my_key").toString()
certificate = fs.readFileSync("my_crt").toString()
ca = fs.readFileSync("intermediate.crt").toString()
app = express.createServer({
    key: privateKey
    cert: certificate
    ca: ca
})
                        

SSL Client


io.connect('https://localhost', {secure: true})
                        

Cross domain?

Yes! (If use WebSocket or JSONP)

Authentication?

Authentication

  • Cookie & Session
  • Cookie does not cross domain, use SSL + Access Token instead

Load Balancing?

Yes! (If use Nginx >= 1.3.13)

Sample Nginx Configuration


server {
    listen 80;
    server_name my.server;
 
    location / {
        proxy_pass nodeCluster;
        proxy_http_version 1.1;
        proxy_set_header Upgrade $http_upgrade;
        proxy_set_header Connection "upgrade";
        proxy_set_header Host $host;
    }
}
                        

Load Balancing (ELB)

  • ELB's HTTP(S) proxy does not understand websocket requests
  • Use TCP/SSL, instead of HTTP/HTTPS
  • No session stickyness
  • No X-Forwarded-For header

Scalability

  • Single thread
  • 2500~3500 connections per process
  • As many processes as you want

Configuration

Some features do not work by default

Configurations

  • store (default: MemoryStore, single process only)
  • transports (default: websocket, htmlfile, xhr-polling, jsonp-polling)
  • authorization (default: false)

Configurations - transports


io.configure "production", ->
    io.enable "browser client etag"
    io.set "log level", 1
    io.set "transports", ["websocket", "flashsocket", "htmlfile", "xhr-polling", "jsonp-polling"]
                        

NODE_ENV=production node app.js
                        

Put your name here and win $1,000 EZTABLE VIP Voucher!

Hint: socket.emit "prize", "your name"

Never trust data from frontend!

Culture

  • Flexible working hours
  • No PM, Product Owner MUST code/design
  • Data-driven customer development
  • Hot girls!

Every Month you got

  • 2~3 study groups
  • 1 work away day
  • 1 extra holiday
  • 1 hack day