Building Real-Time Apps with EmberJS & WebSockets
Building Real-Time Apps with EmberJS & WebSockets
Ben LimmerGEMConf - 5/21/2016 ! ember.party
" blimmer
Ben LimmerEmberJS Meetup - 2/24/2016 ! ember.party
♥
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Talk Roadmap
• WebSockets vs. AJAX
• Fundamentals of WebSockets
• Code!
• Other Considerations
Ben LimmerGEMConf - 5/21/2016 ! ember.party
request
response
request
response
AJAX
Ben LimmerGEMConf - 5/21/2016 ! ember.party
with a lot of apps, this paradigm still works
Ben LimmerGEMConf - 5/21/2016 ! ember.party
but what about real-time apps?
Ben LimmerGEMConf - 5/21/2016 ! ember.party
e.g.
Ben LimmerGEMConf - 5/21/2016 ! ember.party
live dashboards
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Source: http://www.heckyl.com/
Ben LimmerGEMConf - 5/21/2016 ! ember.party
2nd screen apps
Ben LimmerGEMConf - 5/21/2016 ! ember.party© MLB / Source: MLB.com
Ben LimmerGEMConf - 5/21/2016 ! ember.party
deployment notifications
Ben LimmerGEMConf - 5/21/2016 ! ember.party
games
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Source: http://browserquest.mozilla.org/img/common/promo-title.jpg
Ben LimmerGEMConf - 5/21/2016 ! ember.party
chat
gamesdeployment notifications
live dashboards
2nd screen apps
activity streams
comment sections
realtime progresscollaborative
editing
Ben LimmerGEMConf - 5/21/2016 ! ember.party
how do we build a real-time app?
Ben LimmerGEMConf - 5/21/2016 ! ember.party
update?
nope.
(old way) short polling
update?
nope.
dataupdate?
yep!
Ben LimmerGEMConf - 5/21/2016 ! ember.party
(old way) long polling
requestKeep-Alive
timeout
requestKeep-Alive
data
response
requestKeep-Alive
Ben LimmerGEMConf - 5/21/2016 ! ember.party
WebSockets
handshake
connection ope
ned
bi-directionalcommunication
Ben LimmerGEMConf - 5/21/2016 ! ember.party
WebSockets
no polling
full duplex over TCP
communication over standard HTTP(S) ports
broadcast to all connected clients
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Talk Roadmap
• WebSockets vs. AJAX
• Fundamentals of WebSockets
• Code!
• Other Considerations
Ben LimmerGEMConf - 5/21/2016 ! ember.party
the handshake
Ben LimmerGEMConf - 5/21/2016 ! ember.party
RequestGET wss://example.org/socket HTTP/1.1 Origin: https://example.org Host: example.org Sec-WebSocket-Key: zy6Dy9mSAIM7GJZNf9rI1A== Upgrade: websocket Connection: Upgrade Sec-WebSocket-Version: 13
ResponseHTTP/1.1 101 Switching Protocols Connection: Upgrade Sec-WebSocket-Accept: EDJa7WCAQQzMCYNJM42Syuo9SqQ= Upgrade: websocket
events• open • message • error • close
• send • close
methods
Ben LimmerGEMConf - 5/21/2016 ! ember.party
WebSocket.send()
Ben LimmerGEMConf - 5/21/2016 ! ember.party
send(String 'foo');
send(Blob 010101);
send(ArrayBuffer file);
Ben LimmerGEMConf - 5/21/2016 ! ember.party
WebSocket.send(’YOLO’);
Ben LimmerGEMConf - 5/21/2016 ! ember.party
sub-protocols
• a contract between client/server
• 2 classes of sub-protocols
• well-defined (e.g. STOMP, WAMP)
• application specific protocols
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Request with ProtocolGET wss://example.org/socket HTTP/1.1 Origin: https://example.org Host: example.org Sec-WebSocket-Key: zy6Dy9mSAIM7GJZNf9rI1A== Upgrade: websocket Connection: Upgrade Sec-WebSocket-Version: 13 Sec-WebSocket-Protocol: v10.stomp
Ben LimmerGEMConf - 5/21/2016 ! ember.party
STOMPSENDdestination:/queue/a
hello queue a^@
MESSAGEdestination:/queue/amessage-id: <message-identifier>
hello queue a^@
Ben LimmerGEMConf - 5/21/2016 ! ember.party
STOMP
SENDdestination:/queue/a
hello queue a^@
Ben LimmerGEMConf - 5/21/2016 ! ember.party
subprotocols bring structure to ws
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Talk Roadmap
• AJAX vs. WebSockets
• Fundamentals of WebSockets
• Code!
• Other Considerations
Ben LimmerGEMConf - 5/21/2016 ! ember.party
let’s build something!
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerEmberJS Meetup - 2/24/2016 ! ember.party
alice clicks
bob / everyone sees
Ben LimmerEmberJS Meetup - 2/24/2016 ! ember.party
bob clicks
alice / everyone sees
Ben LimmerGEMConf - 5/21/2016 ! ember.party
npm install ws
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
• fast
• simple WebSocket implementation
• few bells and whistles
npm install ws
Ben LimmerGEMConf - 5/21/2016 ! ember.party
server/index.js
1 const WebSocketServer = require('ws').Server; 2 3 const wss = new WebSocketServer({ 4 port: process.env.PORT 5 });
Ben LimmerGEMConf - 5/21/2016 ! ember.party
waiting for socket connection…
Ben LimmerGEMConf - 5/21/2016 ! ember.party
ember install ember-websockets
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
ember install ember-websockets
• integrates with the Ember runloop
• is an Ember.ObjectProxy
• abstracts away the WebSocket
Ben LimmerGEMConf - 5/21/2016 ! ember.party
app/services/rt-ember-socket.js 1 websockets: service(), 2 3 init() { 4 this._super(...arguments); 5 6 const socket = this.get('websockets').socketFor(host); 7 8 socket.on('open', this.open, this); 9 socket.on('close', this.reconnect, this); 10 }, 11 12 online: false, 13 open() { 14 this.set('online', true); 15 }, 16 17 reconnect() { 18 this.set('online', false); 19 20 Ember.run.later(this, () => { 21 this.get('socket').reconnect(); 22 }, 5000); 23 },
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
rtember-1.0 - sub-protocol
Ben LimmerGEMConf - 5/21/2016 ! ember.party
rtember-1.0 - sub-protocol
data events
Ben LimmerGEMConf - 5/21/2016 ! ember.party
server/index.js 1 const WebSocketServer = require('ws').Server; 2 3 const wss = new WebSocketServer({ 4 port: process.env.PORT, 5 handleProtocols: function(protocol, cb) { 6 const supportedProtocol = 7 protocol[protocol.indexOf('rtember-1.0')]; 8 if (supportedProtocol) { 9 cb(true, supportedProtocol); 10 } else { 11 cb(false); 12 } 13 }, 14 });
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
app/services/rt-ember-socket.js
1 websockets: Ember.inject.service(), 2 3 socket: null, 4 init() { 5 this._super(...arguments); 6 7 const socket = this.get('websockets') 8 .socketFor(host, ['rtember-1.0']); 9 10 socket.on('open', this.open, this); 11 socket.on('close', this.reconnect, this); 12 13 this.set('socket', socket); 14 },
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
rtember-1.0 - sub-protocol
data events
Ben LimmerGEMConf - 5/21/2016 ! ember.party
rtember-1.0 - sub-protocol
{ "frameType": "event", "payload": { “eventType": ... event type ..., "eventInfo": ... event info ... }}
{ "frameType": "data", "payload": { ... json api payload ... }}
or
Ben LimmerGEMConf - 5/21/2016 ! ember.party
rtember-1.0 - sub-protocol
data events
Ben LimmerGEMConf - 5/21/2016 ! ember.party
1 wss.on('connection', function(ws) { 2 sendInitialGifs(ws); 3 }); 4 5 function sendInitialGifs(ws) { 6 const gifs = gifDb; 7 const random = _.sampleSize(gifs, 25); 8 9 sendDataToClient(ws, serializeGifs(random)); 10 } 11 12 function sendDataToClient(ws, payload) { 13 const payload = { 14 frameType: FRAME_TYPES.DATA, 15 payload, 16 } 17 ws.send(JSON.stringify(payload)); 18 }
Ben LimmerGEMConf - 5/21/2016 ! ember.party
app/services/rt-ember-socket.js 1 init() { 2 ... 3 socket.on('message', this.handleMessage, this); 4 ... 5 }, 6 7 handleMessage(msg) { 8 const { frameType, payload } = JSON.parse(msg.data); 9 10 if (frameType === FRAME_TYPES.DATA) { 11 this.handleData(payload); 12 } else { 13 warn(`Encountered unknown frame type: ${frameType}`); 14 } 15 }, 16 17 handleData(payload) { 18 this.get('store').pushPayload(payload); 19 }
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
{ "frameType": "data", "payload": { "data": [{ "type": "gif", "id": "3o8doPV2heuYjdN2Fy", "attributes": { "url": "http://giphy.com/3o8doPV2heuYjdN2Fy/giphy.gif" } }, { ... }, { ... }] }}
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
app/routes/index.js
1 export default Ember.Route.extend({ 2 model() { 3 return this.store.peekAll('gif'); 4 } 5 });
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
app/templates/index.hbs{{gif-tv gifs=model}}
app/templates/components/gif-tv.hbs<div class='suggestions'> {{#each gifs as |gif|}} <img src={{gif.url}} /> {{/each}}</div>
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
rtember-1.0 - sub-protocol
data events
Ben LimmerGEMConf - 5/21/2016 ! ember.party
rtember-1.0 - sub-protocol
{ "frameType": "event", "payload": { “eventType": ... event type ..., "eventInfo": ... event info ... }}
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Share GIF Event{ "frameType": "event", "payload": { "eventType": "share_gif", "eventInfo": "<gif_id>" }}
{ "frameType": "data", "payload": {[ <shared_gif>, <previously_shared_gif> ]}}
Ben LimmerGEMConf - 5/21/2016 ! ember.party
{ "frameType": "data", "payload": { "data": [ { "type": "gifs", "id": "3o8doPV2heuYjdN2Fy", "attributes": { "url": "http://giphy.com/3o8doPV2heuYjdN2Fy/giphy.gif", "shared": false } }, { "type": "gifs", "id": "xTiQyBOIQe5cgiyUPS", "attributes": { "url": "http://giphy.com/xTiQyBOIQe5cgiyUPS/giphy.gif", "shared": true } } ] }}
Ben LimmerGEMConf - 5/21/2016 ! ember.party
app/templates/components/gif-tv.hbs<div class='suggestions'> {{#each gifs as |gif|}} <img {{action shareGif gif}} src={{gif.url}} /> {{/each}}</div>
app/components/gif-tv.js 1 export default Ember.Component.extend({ 2 rtEmberSocket: service(), 3 4 shareGif(gif) { 5 this.get('rtEmberSocket') 6 .sendEvent(EVENTS.SHARE_GIF, gif.get('id')); 7 }, 8 });
Ben LimmerGEMConf - 5/21/2016 ! ember.party
5 this.get('rtEmberSocket') 6 .sendEvent(EVENTS.SHARE_GIF, gif.get('id')); 7 }, 8 });
app/services/rt-ember-socket.js 1 sendEvent(eventType, eventInfo) { 2 this.get('socket').send(JSON.stringify({ 3 frameType: FRAME_TYPES.EVENT, 4 payload: { 5 eventType, 6 eventInfo, 7 }, 8 })); 9 }
Ben LimmerEmberJS Meetup - 2/24/2016 ! ember.party
1 ws.on('message', function(rawData) { 2 const data = JSON.parse(rawData); 3 4 if (data.frameType === FRAME_TYPES.EVENT) { 5 const newShare = _.find(gifDb, { 6 id: data.payload.eventInfo 7 }); 8 const previouslyShared = _.find(gifDb, 'shared'); 9 10 newShare.shared = true; 11 previouslyShared.shared = false; 12 13 const framePayload = { 14 frameType: FRAME_TYPES.DATA, 15 payload: serializeGifs([previouslyShared, newShare]), 16 }; 17 const rawPayload = JSON.stringify(framePayload); 18 wss.clients.forEach((client) => { 19 client.send(rawPayload); 20 }); 21 } 22 });
Ben LimmerGEMConf - 5/21/2016 ! ember.party
beware
Ben LimmerGEMConf - 5/21/2016 ! ember.party
1 ws.on('message', function(rawData) { 2 try { 3 const data = JSON.parse(rawData); 4 5 if (data.frameType === FRAME_TYPES.EVENT) { 6 if (data.payload.eventType !== EVENTS.SHARE_GIF) { 7 throw Error(); // unknown event 8 } 9 10 const newShare = ...; 11 if (!newShare) { 12 throw Error(); // unknown gif 13 } 14 ... 15 } 16 } catch(e) { 17 ws.close(1003); // unsupported data 18 } 19 });
Ben LimmerGEMConf - 5/21/2016 ! ember.party
{ "frameType": "data", "payload": { "data": [ { "type": "gifs", "id": "3o8doPV2heuYjdN2Fy", "attributes": { "url": "http://giphy.com/3o8doPV2heuYjdN2Fy/giphy.gif", "shared": false } }, { "type": "gifs", "id": "xTiQyBOIQe5cgiyUPS", "attributes": { "url": "http://giphy.com/xTiQyBOIQe5cgiyUPS/giphy.gif", "shared": true } } ] }}
Ben LimmerGEMConf - 5/21/2016 ! ember.party
app/templates/components/gif-tv.hbs
app/components/gif-tv.js 1 export default Ember.Component.extend({ 2 sharedGif: computed('[email protected]', function() { 3 return this.get('gifs').findBy('shared', true); 4 }), 5 });
<div class='shared-gif'> <img src={{sharedGif.url}} /></div>
<div class='suggestions'> <!-- ... --></div>
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Talk Roadmap
• AJAX vs. WebSockets
• Fundamentals of WebSockets
• Code!
• Other Considerations
Ben LimmerGEMConf - 5/21/2016 ! ember.party
other considerations
• security
• websocket support (libraries)
• learn from example
Ben LimmerGEMConf - 5/21/2016 ! ember.party
security
• Use TLS (wss:// vs. ws://)
• Verify the Origin header
• Verify the request by using a random token on handshake
source: WebSocket (Andrew Lombardi) - O’Reilly
Ben LimmerGEMConf - 5/21/2016 ! ember.party
other considerations
• security
• websocket support (libraries)
• learn from example
Ben LimmerGEMConf - 5/21/2016 ! ember.party
support
Ben LimmerGEMConf - 5/21/2016 ! ember.party
9
Ben LimmerGEMConf - 5/21/2016 ! ember.party
socket.io
• Graceful fallback to polling / flash (!)
• Syntactic Sugar vs. ws package
• Support in ember-websockets add-on
Ben LimmerGEMConf - 5/21/2016 ! ember.party
Ben LimmerGEMConf - 5/21/2016 ! ember.party
pusher.com
• Graceful fallback
• Presence support
• Authentication / Security strategies
• No infrastructure required
Ben LimmerGEMConf - 5/21/2016 ! ember.party
other considerations
• security
• websocket support (libraries)
• learn from example
Ben LimmerGEMConf - 5/21/2016 ! ember.party
learn by example
Ben LimmerGEMConf - 5/21/2016 ! ember.party
learn by example
Ben LimmerGEMConf - 5/21/2016 ! ember.party
https://github.com/blimmer/real-time-ember-clienthttps://github.com/blimmer/real-time-ember-server
# l1m5" blimmer
Ben LimmerGEMConf - 5/21/2016 ! ember.party
thanks!
• WebSocket: Lightweight Client-Server Communications (O’Reilly)
• WebSockets: Methods for Real-Time Data Streaming (Steve Schwartz)
Credits
• pusher.com
• socket.io
• node ws
• websocket security (heroku)
Resources