Server-side Push
Comes of Age
by Brian Sam-Bodden
http://www.integrallis.com
HTTP
HTTP
Unsuspecting Programmers
HTTP
Tim 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 Server
Request
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 Server
Request
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 Server
Request
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 Server
Load the “application”
C Here’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
Polling
iFrame
Streaming
Long
Polling
Flash
Streaming
XHR
Streaming
Java
Applets
Polling
iFrame
Streaming
Long
Polling
Flash
Streaming
XHR
Streaming
Java
Applets
Java
Applets
Polling
Java
Applets
iFrame
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 Server
Request
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
iFrame
Streaming
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
end
end
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
end
end
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
end
end
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
end
end
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
end
end
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
end
end
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 script
function 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 script
function 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 script
function 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 script
function 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 script
function 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
XHR
Streaming
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
end
end
get '/stream' do
stream do |out|
(10..100).step(2) do |n|
out << gimme_funny_word.as_json
sleep 1.5
end
end
end
get '/stream' do
stream do |out|
(10..100).step(2) do |n|
out << gimme_funny_word.as_json
sleep 1.5
end
end
end
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 Server
Request
Response
C
C
Nothing here, but hang on...
... and there you go, good day!
Event
polling
Long
Polling
Demo
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_json
end
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_json
end
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_json
end
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_json
end
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_json
end
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_json
end
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_json
end
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)
end
end
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)
end
end
Difficult to
Implement
Flash
Streaming
Long
Polling
iFrame
Streaming
Java
Applets
Polling
XHR
Streaming
Flash
Streaming
Flash
Streaming
XML Socket
Single Pixel
Flash 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
Comet
Demo
with
http://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
Sockets
Demo
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
end
end
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 msg
end
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
Thanks
All example code available at:
https://github.com/integrallis/server-side-push
Watch out for an upcoming article at http://integrallis.com
by Brian Sam-Bodden
http://www.integrallis.com
http://www.slideshare.net/bsbodden/ssp-oscon

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

  • 1.
    Server-side Push Comes ofAge by Brian Sam-Bodden http://www.integrallis.com
  • 2.
  • 3.
  • 4.
  • 5.
    ... and itwas good
  • 6.
  • 7.
  • 8.
    Web Applications Uhmm, yeah... Rememberthat client-server desktop app? It should be easy to port it to the web, right?
  • 9.
    Client Server Request Response C C Well, hellolet 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
  • 10.
    Client Server Request Response C C Well, hellolet 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
  • 11.
    Client Server Request Response C C Well, hellolet 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
  • 13.
  • 14.
  • 15.
    Client Server Load the“application” C Here’s the initial page load ajax model C ... just what’s changedUser Action #1 C ... and againUser Action #2
  • 16.
    The Web asa Platform
  • 17.
  • 18.
    Push Uhmm, yeah... Remember thatcool web app I underpaid you to build? It should be easy to notify the user when something important happens, right?
  • 19.
  • 20.
  • 21.
  • 22.
  • 23.
  • 24.
  • 25.
  • 26.
  • 27.
  • 28.
  • 29.
  • 31.
  • 32.
  • 33.
  • 34.
  • 35.
  • 36.
  • 37.
  • 38.
  • 39.
    So we had(have) to Hack it
  • 40.
  • 41.
  • 42.
  • 43.
  • 45.
  • 46.
  • 47.
  • 48.
  • 49.
  • 50.
  • 51.
  • 52.
  • 53.
  • 54.
  • 55.
  • 56.
  • 57.
  • 58.
  • 59.
    Oh yes, itinvolves an iFrame #sadpanda
  • 60.
  • 61.
    first you embedan invisible iFrame
  • 62.
    $('<iframe />', { name:'hidden-iframe', id: 'hidden-iframe', src: '/long-running', css: { 'display': 'none' } }).appendTo('body');
  • 63.
    $('<iframe />', { name:'hidden-iframe', id: 'hidden-iframe', src: '/long-running', css: { 'display': 'none' } }).appendTo('body');
  • 64.
    $('<iframe />', { name:'hidden-iframe', id: 'hidden-iframe', src: '/long-running', css: { 'display': 'none' } }).appendTo('body');
  • 65.
    $('<iframe />', { name:'hidden-iframe', id: 'hidden-iframe', src: '/long-running', css: { 'display': 'none' } }).appendTo('body');
  • 66.
    $('<iframe />', { name:'hidden-iframe', id: 'hidden-iframe', src: '/long-running', css: { 'display': 'none' } }).appendTo('body');
  • 67.
    $('<iframe />', { name:'hidden-iframe', id: 'hidden-iframe', src: '/long-running', css: { 'display': 'none' } }).appendTo('body');
  • 68.
    on the server youneed streaming capabilities
  • 69.
    get '/long-running' do streamdo |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 end end
  • 70.
    get '/long-running' do streamdo |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 end end
  • 71.
    get '/long-running' do streamdo |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 end end
  • 72.
    get '/long-running' do streamdo |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 end end
  • 73.
    get '/long-running' do streamdo |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 end end
  • 74.
    get '/long-running' do streamdo |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 end end
  • 75.
    def update_progress(percent, word,meaning) %[<script type="text/javascript"> parent.updatePage(#{percent}, '#{word}', '#{meaning}'); </script>] end
  • 76.
    def update_progress(percent, word,meaning) %[<script type="text/javascript"> parent.updatePage(#{percent}, '#{word}', '#{meaning}'); </script>] end
  • 77.
    // called bythe streamed server-sent script function 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!'); } }
  • 78.
    // called bythe streamed server-sent script function 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!'); } }
  • 79.
    // called bythe streamed server-sent script function 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!'); } }
  • 80.
    // called bythe streamed server-sent script function 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!'); } }
  • 81.
    // called bythe streamed server-sent script function 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!'); } }
  • 82.
  • 83.
  • 84.
  • 85.
  • 86.
  • 87.
  • 88.
  • 89.
  • 90.
    get '/stream' do streamdo |out| (10..100).step(2) do |n| out << gimme_funny_word.as_json sleep 1.5 end end end
  • 91.
    get '/stream' do streamdo |out| (10..100).step(2) do |n| out << gimme_funny_word.as_json sleep 1.5 end end end
  • 92.
    get '/stream' do streamdo |out| (10..100).step(2) do |n| out << gimme_funny_word.as_json sleep 1.5 end end end
  • 93.
  • 94.
    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);
  • 95.
    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);
  • 96.
    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);
  • 97.
    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);
  • 98.
    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);
  • 99.
    frequency of pollingthe stream >= server serving rate
  • 100.
  • 101.
  • 102.
  • 103.
  • 104.
  • 105.
  • 106.
  • 107.
    Client Server Request Response C C Nothing here,but hang on... ... and there you go, good day! Event polling
  • 108.
  • 109.
    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_json end
  • 110.
    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_json end
  • 111.
    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_json end
  • 112.
    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_json end
  • 113.
    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_json end
  • 114.
    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_json end
  • 115.
    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_json end
  • 116.
    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); } }); }
  • 117.
    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); } }); }
  • 118.
    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); } }); }
  • 119.
    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); } }); }
  • 120.
    Naive Long pollingw/ 10sec timeout { Requests that returned data Current polling request Requests in RED are timed out long polls
  • 121.
    There is abig issue with the previous example...
  • 122.
    There is abig issue with the previous example... Besides using a Text File as a database
  • 123.
    The server doesn’tsupport async responses...
  • 124.
    The busy IOchecking loop will block
  • 125.
    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) end end
  • 126.
    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) end end
  • 127.
  • 128.
  • 129.
  • 130.
  • 131.
  • 132.
  • 133.
  • 134.
  • 135.
  • 136.
  • 137.
    Provide both Client andServer Components
  • 138.
  • 139.
  • 140.
  • 141.
    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(''); });
  • 142.
    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(''); });
  • 143.
    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(''); });
  • 144.
    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(''); });
  • 145.
    Now we cancreate a room ... and have a conversation
  • 147.
  • 148.
  • 149.
  • 150.
  • 151.
    with security, proxies &firewalls in mind
  • 152.
  • 153.
    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 end end em-websocket provides an easy to use WebSocket class On the server we’ll implement some WebSocket event handlers
  • 154.
    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
  • 155.
    ws.onmessage do |msg| @messages<< msg @messages.shift if @messages.length > 10 @channel.push msg end 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
  • 156.
  • 157.
    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”
  • 158.
    <div class="container"> <h1 class="visible-desktop">WebSocketsSinatra 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
  • 159.
    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
  • 160.
    $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
  • 161.
    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); }
  • 162.
    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)
  • 163.
    On Firefox On SafariDesktop ... and on my almost out of batteries iPhone
  • 164.
  • 165.
  • 166.
  • 167.
  • 168.
  • 169.
  • 170.
    That plays well withyour framework
  • 171.
    Thanks All example codeavailable at: https://github.com/integrallis/server-side-push Watch out for an upcoming article at http://integrallis.com by Brian Sam-Bodden http://www.integrallis.com http://www.slideshare.net/bsbodden/ssp-oscon