Server-Side Push: Comet, Web Sockets come of age (OSCON 2013)

Post on 08-May-2015

5.108 views 0 download

description

Server-side browser push technologies have been around for a while in one way or another, ranging from from crude browser polling to Flash enabled frameworks. In this session you’ll get a code-driven walk-through on the evolution and mechanics of server-push technologies, including: Server streaming Polling and long Polling Comet Web Sockets

Transcript of Server-Side Push: Comet, Web Sockets come of age (OSCON 2013)

Server-side Push

Comes of Age

by Brian Sam-Boddenhttp://www.integrallis.com

HTTP

HTTPUnsuspecting Programmers

HTTPTim Berners-Lee

Unsuspecting Programmers

... and it was good

...for documents

it scales!

Web Applications

Uhmm, yeah...Remember that client-server desktop app?It should be easy to port it to the web, right?

Client ServerRequest

Response

C

C

Well, hello let me make you a page good friend

... don’t forget the images

page-by-page model

C... hold on, I got hit the DB

C... and cook up some HTML

C... and all other assets

Client ServerRequest

Response

C

C

Well, hello let me make you a page good friend

... don’t forget the images

page-by-page model

C... hold on, I got hit the DB

C... and cook up some HTML

C... and all other assets

Client ServerRequest

Response

C

C

Well, hello let me make you a page good friend

... don’t forget the images

page-by-page model

C... hold on, I got hit the DB

C... and cook up some HTML

C... and all other assets

ask for it background

and change the relevant bits

Client ServerLoad the “application”

CHere’s the initial page load

ajax model

C... just what’s changedUser Action #1

C... and againUser Action #2

The Web as a Platform

So things got better, until ...

Push

Uhmm, yeah...Remember that cool web app I underpaid you to build?

It should be easy to notify the user when something important happens, right?

PUSH

Why?

Collaboration

Chat

Comments

Notifications

Bidding Platforms

Monitoring

Stocks

Scores

Games

Bi-directional

Asynchronous

Near Real-Time

Server Initiated*

Communications

But...

until fairly recently

Browsers Sucked!

So we had (have) to Hack it

Java Applets

PollingiFrame

Streaming

Long Polling

Flash Streaming

XHR Streaming

Java Applets

PollingiFrame

Streaming

Long Polling

Flash Streaming

XHR Streaming

Java Applets

Java Applets

PollingJava

AppletsiFrame

Streaming

Long Polling

Flash Streaming

XHR Streaming

Polling

Polling

are we there yet?

setInterval(function() { areWeThereYet();}, 1000);

setInterval(function() { areWeThereYet();}, 1000);

setInterval(function() { areWeThereYet();}, 1000);

setInterval(function() { areWeThereYet();}, 1000);

Client ServerRequest

Response

Request

Response

C

C

No Soup for you!

Ok, here you go

Event

polling

chatty / high traffic

Self-inflicted DDOS Attack!

iFrame Streaming

Java Applets

Polling

Long Polling

Flash Streaming

XHR Streaming

iFrame Streaming

iFrame Streaming

Oh yes, it involves an iFrame

#sadpanda

iFrameStreaming

Demo

first you embed an invisible iFrame

$('<iframe />', { name: 'hidden-iframe', id: 'hidden-iframe', src: '/long-running', css: { 'display': 'none' } }).appendTo('body');

$('<iframe />', { name: 'hidden-iframe', id: 'hidden-iframe', src: '/long-running', css: { 'display': 'none' } }).appendTo('body');

$('<iframe />', { name: 'hidden-iframe', id: 'hidden-iframe', src: '/long-running', css: { 'display': 'none' } }).appendTo('body');

$('<iframe />', { name: 'hidden-iframe', id: 'hidden-iframe', src: '/long-running', css: { 'display': 'none' } }).appendTo('body');

$('<iframe />', { name: 'hidden-iframe', id: 'hidden-iframe', src: '/long-running', css: { 'display': 'none' } }).appendTo('body');

$('<iframe />', { name: 'hidden-iframe', id: 'hidden-iframe', src: '/long-running', css: { 'display': 'none' } }).appendTo('body');

on the server you need streaming capabilities

get '/long-running' do stream do |out| (10..100).step(10) do |n| word = gimme_funny_word out << update_progress(n, word.keys.first, word.values.first) sleep 1.5 end endend

get '/long-running' do stream do |out| (10..100).step(10) do |n| word = gimme_funny_word out << update_progress(n, word.keys.first, word.values.first) sleep 1.5 end endend

get '/long-running' do stream do |out| (10..100).step(10) do |n| word = gimme_funny_word out << update_progress(n, word.keys.first, word.values.first) sleep 1.5 end endend

get '/long-running' do stream do |out| (10..100).step(10) do |n| word = gimme_funny_word out << update_progress(n, word.keys.first, word.values.first) sleep 1.5 end endend

get '/long-running' do stream do |out| (10..100).step(10) do |n| word = gimme_funny_word out << update_progress(n, word.keys.first, word.values.first) sleep 1.5 end endend

get '/long-running' do stream do |out| (10..100).step(10) do |n| word = gimme_funny_word out << update_progress(n, word.keys.first, word.values.first) sleep 1.5 end endend

def update_progress(percent, word, meaning) %[<script type="text/javascript"> parent.updatePage(#{percent}, '#{word}', '#{meaning}'); </script>]end

def update_progress(percent, word, meaning) %[<script type="text/javascript"> parent.updatePage(#{percent}, '#{word}', '#{meaning}'); </script>]end

// called by the streamed server-sent scriptfunction updatePage(percent, word, meaning) { $kickItButton.text(percent + '%'); $theWord.text(word); $theMeaning.text(meaning); if (percent == 100) { $kickItButton.attr('disabled', false); $kickItButton.text('Kick it again!'); }}

// called by the streamed server-sent scriptfunction updatePage(percent, word, meaning) { $kickItButton.text(percent + '%'); $theWord.text(word); $theMeaning.text(meaning); if (percent == 100) { $kickItButton.attr('disabled', false); $kickItButton.text('Kick it again!'); }}

// called by the streamed server-sent scriptfunction updatePage(percent, word, meaning) { $kickItButton.text(percent + '%'); $theWord.text(word); $theMeaning.text(meaning); if (percent == 100) { $kickItButton.attr('disabled', false); $kickItButton.text('Kick it again!'); }}

// called by the streamed server-sent scriptfunction updatePage(percent, word, meaning) { $kickItButton.text(percent + '%'); $theWord.text(word); $theMeaning.text(meaning); if (percent == 100) { $kickItButton.attr('disabled', false); $kickItButton.text('Kick it again!'); }}

// called by the streamed server-sent scriptfunction updatePage(percent, word, meaning) { $kickItButton.text(percent + '%'); $theWord.text(word); $theMeaning.text(meaning); if (percent == 100) { $kickItButton.attr('disabled', false); $kickItButton.text('Kick it again!'); }}

Drawback:Page is ‘forever’

loading

iFrame Streaming

Java Applets

Polling

Long Polling

Flash Streaming

XHR Streaming

XHR Streaming

XHR Streaming

XHRStreaming

Demo

better than iframes

use AJAX call

send JSON

get '/stream' do stream do |out| (10..100).step(2) do |n| out << gimme_funny_word.as_json sleep 1.5 end endend

get '/stream' do stream do |out| (10..100).step(2) do |n| out << gimme_funny_word.as_json sleep 1.5 end endend

get '/stream' do stream do |out| (10..100).step(2) do |n| out << gimme_funny_word.as_json sleep 1.5 end endend

polling the stream

parse = function() { // parse the xhr.responseText and update the UI }; xhr = new XMLHttpRequest();url = "/stream";xhr.open("GET", url, true);xhr.send(); last_index = 0;interval = setInterval(parse, 500);setTimeout((function() { clearInterval(interval); parse(); xhr.abort(); }), 20000);

parse = function() { // parse the xhr.responseText and update the UI }; xhr = new XMLHttpRequest();url = "/stream";xhr.open("GET", url, true);xhr.send(); last_index = 0;interval = setInterval(parse, 500);setTimeout((function() { clearInterval(interval); parse(); xhr.abort(); }), 20000);

parse = function() { // parse the xhr.responseText and update the UI }; xhr = new XMLHttpRequest();url = "/stream";xhr.open("GET", url, true);xhr.send(); last_index = 0;interval = setInterval(parse, 500);setTimeout((function() { clearInterval(interval); parse(); xhr.abort(); }), 20000);

parse = function() { // parse the xhr.responseText and update the UI }; xhr = new XMLHttpRequest();url = "/stream";xhr.open("GET", url, true);xhr.send(); last_index = 0;interval = setInterval(parse, 500);setTimeout((function() { clearInterval(interval); parse(); xhr.abort(); }), 20000);

parse = function() { // parse the xhr.responseText and update the UI }; xhr = new XMLHttpRequest();url = "/stream";xhr.open("GET", url, true);xhr.send(); last_index = 0;interval = setInterval(parse, 500);setTimeout((function() { clearInterval(interval); parse(); xhr.abort(); }), 20000);

frequency of polling the stream >=

server serving rate

No Throbber Freakout

Long Polling

iFrame Streaming

Java Applets

Polling

Flash Streaming

XHR Streaming

Long Polling

Long Polling

most commonly used

response is blocked...

...until server event occurs

Client ServerRequest

Response

C

C

Nothing here, but hang on...

... and there you go, good day!Event

polling

Long PollingDemo

get '/read' do content_type :json filename = 'data.txt'

last = params[:timestamp] == 'null' ? 0 : params[:timestamp].to_i current = last_modification(filename) not_changed_or_emtpy = true while (not_changed_or_emtpy) do sleep 0.1 not_changed_or_emtpy = File.zero?(filename) || (current <= last) current = last_modification(filename) end

{ :messages => File.read(filename), :timestamp => current }.to_jsonend

get '/read' do content_type :json filename = 'data.txt'

last = params[:timestamp] == 'null' ? 0 : params[:timestamp].to_i current = last_modification(filename) not_changed_or_emtpy = true while (not_changed_or_emtpy) do sleep 0.1 not_changed_or_emtpy = File.zero?(filename) || (current <= last) current = last_modification(filename) end

{ :messages => File.read(filename), :timestamp => current }.to_jsonend

get '/read' do content_type :json filename = 'data.txt'

last = params[:timestamp] == 'null' ? 0 : params[:timestamp].to_i current = last_modification(filename) not_changed_or_emtpy = true while (not_changed_or_emtpy) do sleep 0.1 not_changed_or_emtpy = File.zero?(filename) || (current <= last) current = last_modification(filename) end

{ :messages => File.read(filename), :timestamp => current }.to_jsonend

get '/read' do content_type :json filename = 'data.txt'

last = params[:timestamp] == 'null' ? 0 : params[:timestamp].to_i current = last_modification(filename) not_changed_or_emtpy = true while (not_changed_or_emtpy) do sleep 0.1 not_changed_or_emtpy = File.zero?(filename) || (current <= last) current = last_modification(filename) end

{ :messages => File.read(filename), :timestamp => current }.to_jsonend

get '/read' do content_type :json filename = 'data.txt'

last = params[:timestamp] == 'null' ? 0 : params[:timestamp].to_i current = last_modification(filename) not_changed_or_emtpy = true while (not_changed_or_emtpy) do sleep 0.1 not_changed_or_emtpy = File.zero?(filename) || (current <= last) current = last_modification(filename) end

{ :messages => File.read(filename), :timestamp => current }.to_jsonend

get '/read' do content_type :json filename = 'data.txt'

last = params[:timestamp] == 'null' ? 0 : params[:timestamp].to_i current = last_modification(filename) not_changed_or_emtpy = true while (not_changed_or_emtpy) do sleep 0.1 not_changed_or_emtpy = File.zero?(filename) || (current <= last) current = last_modification(filename) end

{ :messages => File.read(filename), :timestamp => current }.to_jsonend

get '/read' do content_type :json filename = 'data.txt'

last = params[:timestamp] == 'null' ? 0 : params[:timestamp].to_i current = last_modification(filename) not_changed_or_emtpy = true while (not_changed_or_emtpy) do sleep 0.1 not_changed_or_emtpy = File.zero?(filename) || (current <= last) current = last_modification(filename) end

{ :messages => File.read(filename), :timestamp => current }.to_jsonend

function longPoll() { $.ajax({ type : 'get', url : '/read?timestamp=' + timestamp, async : true, cache : false, timeout : 10000,

success : function(json) { var messages = json['messages'].split("\n") var last = messages[messages.length-1]; if (last) { $('#msg').append('<div>'+last+'</div>'); timestamp = json['timestamp']; } setTimeout(longPoll, 1000); }, error : function(xhr, textStatus, error) { setTimeout(longPoll, 2000); } });}

function longPoll() { $.ajax({ type : 'get', url : '/read?timestamp=' + timestamp, async : true, cache : false, timeout : 10000,

success : function(json) { var messages = json['messages'].split("\n") var last = messages[messages.length-1]; if (last) { $('#msg').append('<div>'+last+'</div>'); timestamp = json['timestamp']; } setTimeout(longPoll, 1000); }, error : function(xhr, textStatus, error) { setTimeout(longPoll, 2000); } });}

function longPoll() { $.ajax({ type : 'get', url : '/read?timestamp=' + timestamp, async : true, cache : false, timeout : 10000,

success : function(json) { var messages = json['messages'].split("\n") var last = messages[messages.length-1]; if (last) { $('#msg').append('<div>'+last+'</div>'); timestamp = json['timestamp']; } setTimeout(longPoll, 1000); }, error : function(xhr, textStatus, error) { setTimeout(longPoll, 2000); } });}

function longPoll() { $.ajax({ type : 'get', url : '/read?timestamp=' + timestamp, async : true, cache : false, timeout : 10000,

success : function(json) { var messages = json['messages'].split("\n") var last = messages[messages.length-1]; if (last) { $('#msg').append('<div>'+last+'</div>'); timestamp = json['timestamp']; } setTimeout(longPoll, 1000); }, error : function(xhr, textStatus, error) { setTimeout(longPoll, 2000); } });}

Naive Long polling w/ 10sec timeout

{

Requests that returned data

Current polling request

Requests in RED are timed out

long polls

There is a big issue with the previous example...

There is a big issue with the previous example...

Besides using a Text File as a database

The server doesn’t support async responses...

The busy IO checking loop will block

aget '/read' do content_type :json filename = 'data.txt'

last = params[:timestamp] == 'null' ? 0 : params[:timestamp].to_i current = last_modification(filename) EM.defer do check_file_changes = proc do if File.zero?(filename) || (current <= last) current = last_modification(filename) EM.next_tick(&check_file_changes) else body({ :messages => File.read(filename), :timestamp => current }.to_json) end end EM.next_tick(&check_file_changes) endend

aget '/read' do content_type :json filename = 'data.txt'

last = params[:timestamp] == 'null' ? 0 : params[:timestamp].to_i current = last_modification(filename) EM.defer do check_file_changes = proc do if File.zero?(filename) || (current <= last) current = last_modification(filename) EM.next_tick(&check_file_changes) else body({ :messages => File.read(filename), :timestamp => current }.to_json) end end EM.next_tick(&check_file_changes) endend

Difficult to Implement

Flash Streaming

Long Polling

iFrame Streaming

Java Applets

Polling

XHR Streaming

Flash Streaming

Flash Streaming

XML Socket

Single PixelFlash Movie

Go Away Flash!

Push Frameworks

Comet !=

Just Long Polling

Amalgamation of Techniques

Provide both Client and Server

Components

Many use a Pub-Sub Protocol

Bayeaux

CometDemowithhttp://faye.jcoglan.com

var client = new Faye.Client('/faye');

var subscription = client.subscribe('/<%= room %>', function(message) { $messages = $('#messages'); $message = $('<div>' + message['user'] + ' : ' + message['text'] +'</div>') $messages.append($message); }); $("#chat-form").submit(function(e){ e.preventDefault(); var message = $('#message').val(); client.publish('/<%= room %>', {user: '<%= username %>', text: message}); $('#message').val('');});

var client = new Faye.Client('/faye');

var subscription = client.subscribe('/<%= room %>', function(message) { $messages = $('#messages'); $message = $('<div>' + message['user'] + ' : ' + message['text'] +'</div>') $messages.append($message); }); $("#chat-form").submit(function(e){ e.preventDefault(); var message = $('#message').val(); client.publish('/<%= room %>', {user: '<%= username %>', text: message}); $('#message').val('');});

var client = new Faye.Client('/faye');

var subscription = client.subscribe('/<%= room %>', function(message) { $messages = $('#messages'); $message = $('<div>' + message['user'] + ' : ' + message['text'] +'</div>') $messages.append($message); }); $("#chat-form").submit(function(e){ e.preventDefault(); var message = $('#message').val(); client.publish('/<%= room %>', {user: '<%= username %>', text: message}); $('#message').val('');});

var client = new Faye.Client('/faye');

var subscription = client.subscribe('/<%= room %>', function(message) { $messages = $('#messages'); $message = $('<div>' + message['user'] + ' : ' + message['text'] +'</div>') $messages.append($message); }); $("#chat-form").submit(function(e){ e.preventDefault(); var message = $('#message').val(); client.publish('/<%= room %>', {user: '<%= username %>', text: message}); $('#message').val('');});

Now we can create a room ... and have a conversation

Web Sockets

Two Way Communications

Over a dedicated socket

in simple way

with security, proxies & firewalls

in mind

Web SocketsDemo

EventMachine.run do EventMachine::WebSocket.start(:host => "0.0.0.0", :port => 8080) do |ws| ws.onopen do

end

ws.onmessage do |msg|

end

ws.onclose do

end endend

em-websocket provides an easy to use WebSocket class

On the server we’ll implement

some WebSocket event handlers

EventMachine.run do @channel = EM::Channel.new @users = {} @messages = []... ws.onopen do new_user = @channel.subscribe { |msg| ws.send msg } @users[ws.object_id] = new_user @messages.each do |message| ws.send message end end

subscribe a new user to the channel passing the callback to our push action

we’ll keep a list of users in a Hash keyed by the object_id of the incoming ws connection

push the last batch of messages to the user

ws.onmessage do |msg| @messages << msg @messages.shift if @messages.length > 10

@channel.push msgend

add the new message to the end of the queue

broadcast the message to all users connected to the channel

we’ll keep the last 10 messages

ws.onclose do @channel.unsubscribe(@users[ws.object_id]) @users.delete(ws.object_id)end

we unsubscribe them from the channel

remove them from the Hash of users

EventMachine.run do EventMachine::WebSocket.start(...) do |ws| ... end

class App < Sinatra::Base get '/' do erb :index end end App.run!end

our single page application is contained in /public/views/index.erb

The Sinatra app runs as part of the EV “Reactor Loop”

<div class="container"> <h1 class="visible-desktop">WebSockets Sinatra Draw</h1> <legend>Draw Something</legend> <div id="whiteboard" class="well well-small"> <canvas id="draw-canvas"></canvas> </div> </div>

We’ll nest the canvas in a div in order to resize it

correctly

$(document).ready(function() { var $canvas = $('#draw-canvas'); var ws = new WebSocket("ws://" + location.hostname + ":8080");

When the document is ready we’ll connect to the EM Websocket server

running on :8080

var currentX = 0;var currentY = 0;var lastX, lastY, lastReceivedX, lastReceivedY; var drawing = false;var ctx = $('#draw-canvas')[0].getContext('2d');

We’ll grab the 2D canvas context in order to draw on it

$canvas.bind('mousemove',function(ev){ ev = ev || window.event; currentX = ev.pageX - $canvas.offset().left; currentY = ev.pageY - $canvas.offset().top;});

$canvas.bind('touchmove',function(ev){ var touch = ev.originalEvent.touches[0] || ev.originalEvent.changedTouches[0]; currentX = touch.pageX - $canvas.offset().left; currentY = touch.pageY - $canvas.offset().top; });

We’ll update the currentX and currentY coordinates of the mouse over the canvas both for

desktop and mobile browsers

touchmove is provided by jQuery-Mobile-Events

plugin

$canvas.bind('tapstart',function(ev) { drawing = true}); $canvas.bind('tapend',function(ev) { drawing = false});

tapstart and tapend are also provided by the jQuery-Mobile-Events

ws.onopen = function(event) { setInterval(function() { if ((currentX !== lastX || currentY !== lastY) && drawing) { lastX = currentX; lastY = currentY; ws.send(JSON.stringify({ x: currentX, y: currentY})); } }, 30);}

ws.onmessage = function(event) { var msg = $.parseJSON(event.data); ctx.beginPath(); ctx.moveTo(lastReceivedX, lastReceivedY); ctx.lineTo(msg.x, msg.y); ctx.closePath(); ctx.stroke();

lastReceivedX = msg.x; lastReceivedY = msg.y;};

We’ll only draw indirectly when we receive a message (even when we are the ones doing the drawing)

On Firefox

On Safari Desktop

... and on my almost out of batteries iPhone

What are we missing?

Server-sent Events

BOSH

WebRTC

What should you do?

Use a Framework!

That plays well with your

framework

ThanksAll example code available at:

https://github.com/integrallis/server-side-push

Watch out for an upcoming article at http://integrallis.com

by Brian Sam-Boddenhttp://www.integrallis.com

http://www.slideshare.net/bsbodden/ssp-oscon