Building Realtime Apps with Ember.js and WebSockets

Post on 14-Jan-2017

910 views 2 download

transcript

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

Source: inbox.google.com

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('gifs.@each.shared', 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

ember.party/gemconf

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