Upload
brian-sam-bodden
View
5.108
Download
0
Embed Size (px)
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
Citation preview
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
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