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
9. 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
10. 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
11. 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
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
18. 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?
69. 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
70. 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
71. 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
72. 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
73. 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
74. 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
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 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!');
}
}
78. // 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!');
}
}
79. // 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!');
}
}
80. // 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!');
}
}
81. // 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!');
}
}
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 polling w/ 10sec timeout
{
Requests that
returned data
Current polling
request
Requests in RED
are timed out
long polls
121. There is a big issue with
the previous example...
122. There is a big issue with
the previous example...
Besides using a Text
File as a database
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
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
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">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
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
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)
171. 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