KEMBAR78
Server Side Events | PDF
WHAT just happened?
Reacting to Events on the Server
Administrative Notes
• @pilif on twitter
• pilif on github
• working at Sensational AG
• @pilif on twitter
• pilif on github
• working at Sensational AG
• strongly dislike shirts
"💩".length() is still 2
Though there is a lot of good news with ES6
There will be some ☕
Demo first™
Server-Side Events
• Inform users about stuff happening while they are
using the site
• Edits made to the current resource by other people
• Chat Messages
• Mobile Devices interacting with the Account
Additional Constraints
• Must not lose events
• Events must be unique
• Must work with shared sessions
• Separate channels per user
• Must work* even when hand-written daemons are down
• Must work* in development without massaging daemons
Not losing events
• Race condition between event happening and
infrastructure coming up on page load
• Need to persist events
• Using a database
• Using a sequence (auto increment ID) to identify last
sent event
• Falling back to timestamps if not available (initial page
load)
But back to the topic
TIMTWWTDI
*
• Short Polling
• Long Polling
• EventSource
• Web Sockets
* yes. I’m that old
Short Polling
• Are we there yet?
• Are we there yet?
• Are we there yet?
• And now?
Long Polling
• Send a Query to the Server
• Have the server only* reply when an event is available
• Keep the connection open otherwise
• Response means: event has happened
• Have the client reconnect immediately
Server-Sent Events
• http://www.w3.org/TR/eventsource/
• Keeping a connection open to the server
• Server is sending data as text/event-stream
• Colon-separated key-value data.
• Empty line separates events.
WebSockets
• «TCP/IP in your browser»*
• Full Duplex
• Message passing
• Persistent connection between Browser and Server
Let’s try them
Surprise Demo™also Demo first™
index.html
<script>
(function(){
var channel = new EventChannel();
var log_ws = $('#websockets');
$(channel.bind('cheese_created', function(e){
log_ws.prepend($(‘<li>').text(
e.pieces + ' pieces of ' + e.cheese_type
));
});
})();
</script>
index.html
<script>
(function(){
var channel = new EventChannel();
var log_ws = $('#websockets');
$(channel.bind('cheese_created', function(e){
log_ws.prepend($(‘<li>').text(
e.pieces + ' pieces of ' + e.cheese_type
));
});
})();
</script>
index.html
<script>
(function(){
var channel = new EventChannel();
var log_ws = $('#websockets');
$(channel.bind('cheese_created', function(e){
log_ws.prepend($(‘<li>').text(
e.pieces + ' pieces of ' + e.cheese_type
));
});
})();
</script>
index.html
<script>
(function(){
var channel = new EventChannel();
var log_ws = $('#websockets');
$(channel.bind('cheese_created', function(e){
log_ws.prepend($(‘<li>').text(
e.pieces + ' pieces of ' + e.cheese_type
));
});
})();
</script>
publish.js
• Creates between 5 and 120
pieces of random Swiss cheese
• Publishes an event about this
• We’re using redis as our Pub/Sub
mechanism, but you could use
other solutions too
• Sorry for the indentation, but
code had to fit the slide
var cheese_types = ['Emmentaler',
'Appenzeller', 'Gruyère',
'Vacherin', ‘Sprinz'
];
function create_cheese(){
return {
pieces: Math.floor(Math.random()
* 115) + 5,
cheese_type:
cheese_types[Math.floor(
Math.random()
*cheese_types.length
)]
}
}
var cheese_delivery =
create_cheese();
publish(cheese_delivery);
Web Sockets
Server
• Do not try this at home
• Use a library.You might know of socket.io – Me
personally, I used ws.
• Our code: only 32 lines.
This is it
var WebSocketServer = require('ws').Server;
var redis = require('redis');
var wss = new WebSocketServer({port: 8080});
wss.on('connection', function(ws) {
var client = redis.createClient(6379, 'localhost');
ws.on('close', function(){
client.end();
});
client.select(2, function(err, result){
if (err) {
console.log("Failed to set redis database");
return;
}
client.subscribe('channels:cheese');
client.on('message', function(chn, message){
ws.send(message);
});
})
});
Actually, this is the meat
client.subscribe('channels:cheese');
client.on('message', function(chn, message){
ws.send(message);
});
And here’s the client
(function(window){
window.EventChannelWs = function(){
var socket = new WebSocket("ws://localhost:8080/");
var self = this;
socket.onmessage = function(evt){
var event_info = JSON.parse(evt.data);
var evt = jQuery.Event(event_info.type, event_info.data);
$(self).trigger(evt);
}
}
})(window);
And here’s the client
(function(window){
window.EventChannelWs = function(){
var socket = new WebSocket("ws://localhost:8080/");
var self = this;
socket.onmessage = function(evt){
var event_info = JSON.parse(evt.data);
var evt = jQuery.Event(event_info.type, event_info.data);
$(self).trigger(evt);
}
}
})(window);
And here’s the client
(function(window){
window.EventChannelWs = function(){
var socket = new WebSocket("ws://localhost:8080/");
var self = this;
socket.onmessage = function(evt){
var event_info = JSON.parse(evt.data);
var evt = jQuery.Event(event_info.type, event_info.data);
$(self).trigger(evt);
}
}
})(window);
And here’s the client
(function(window){
window.EventChannelWs = function(){
var socket = new WebSocket("ws://localhost:8080/");
var self = this;
socket.onmessage = function(evt){
var event_info = JSON.parse(evt.data);
var evt = jQuery.Event(event_info.type, event_info.data);
$(self).trigger(evt);
}
}
})(window);
Sample was very simple
• No synchronisation with server for initial event
• No fallback when the web socket server is down
• No reverse proxy involved
• No channel separation
Flip-Side
PoweringYour 39 Lines
• 6K lines of JavaScript code
• Plus 3.3K lines of C code
• Plus 508 lines of C++ code
• Which is the body that you actually run (excluding
tests and benchmarks)
• Some of which redundant because NPM
WebSockets are a bloody mess™
• RFC6455 is 71 pages long
• Adding a lot of bit twiddling to intentionally break
proxy servers
• Proxies that work might only actually work*
• Many deployments require a special port to run over
Use SSL.
By the Gods. Use SSL
EventSource
Client
var cheese_channel = new EventSource(url);
var log_source = $('#eventsource');
cheese_channel.addEventListener('cheese_created', function(e){
var data = JSON.parse(e.data);
log_source.prepend($(‘<li>').text(
data.pieces + ' pieces of ' + data.cheese_type
));
});
Client
var cheese_channel = new EventSource(url);
var log_source = $('#eventsource');
cheese_channel.addEventListener('cheese_created', function(e){
var data = JSON.parse(e.data);
log_source.prepend($(‘<li>').text(
data.pieces + ' pieces of ' + data.cheese_type
));
});
Client
var cheese_channel = new EventSource(url);
var log_source = $('#eventsource');
cheese_channel.addEventListener('cheese_created', function(e){
var data = JSON.parse(e.data);
log_source.prepend($(‘<li>').text(
data.pieces + ' pieces of ' + data.cheese_type
));
});
Client
var cheese_channel = new EventSource(url);
var log_source = $('#eventsource');
cheese_channel.addEventListener('cheese_created', function(e){
var data = JSON.parse(e.data);
log_source.prepend($(‘<li>').text(
data.pieces + ' pieces of ' + data.cheese_type
));
});
Server
• Keeps the connection open
• Sends blank-line separated groups of key/value pairs as events happen
• Can tell the client how long to wait when reconnecting
Disillusioning
• Bound to the 6-connections per host rule
• Still needs manual synchronising if you don’t want to lose events
• Browser support is as always
Disillusioning
• Bound to the 6-connections per host rule
• Still needs manual synchronising if you don’t want to lose events
• Browser support is as always
Long Polling
I like it
• Works even with IE6 (god forbid you have to do this)
• Works fine with proxies
• On both ends
• Works fine over HTTP
• Needs some help due to the connection limit
• Works even when your infrastructure is down*
Production code
• The following code samples form the basis of the
initial demo
• It’s production code
• No support issue caused by this.
• Runs fine in a developer-hostile environment
Caution: ☕ ahead
Synchronising using the
database
events_since_id = (channel, id, cb)->
q = """
select * from events
where channel_id = $1 and id > $2
order by id asc
"""
query q, [channel, id], cb
events_since_time = (channel, ts, cb)->
q = """
select * from events o
where channel_id = $1
and ts > (SELECT TIMESTAMP WITH TIME ZONE 'epoch'
+ $2 * INTERVAL '1 second’
)
order by id asc
"""
query q, [channel, ts], cb
The meat
handle_subscription = (c, message)->
fetch_events channel, last_event_id, (err, evts)->
return http_error 500, 'Failed to get event data' if err
abort_processing = write res, evts, true
last_event_id = evts[evts.length-1].id if (evts and evts.length > 0)
if abort_processing
unsubscribe channel, handle_subscription
clear_waiting()
res.end()
fetch_events channel, last_event_id, (err, evts)->
return http_error res, 500, 'Failed to get event data: ' + err if err
last_event_id = evts[evts.length-1].id if (evts and evts.length > 0)
if waiting() or (evts and evts.length > 0)
abort_processing = write(res, evts, not waiting());
if waiting() or abort_processing
unsubscribe channel, handle_subscription
res.end()
set_waiting()
subscribe channel, handle_subscription
if waiting() or (evts and evts.length > 0)
abort_processing = write(res, evts, not waiting());
if waiting() or abort_processing
unsubscribe channel, handle_subscription
res.end()
• If events are pending
• Or if there’s already a connection waiting for the same
channel
• Then return the event data immediately
• And tell the client when to reconnect
• The abort_processing mess is because of support for
both EventSource and long-polling
if waiting() or (evts and evts.length > 0)
abort_processing = write(res, evts, not waiting());
if waiting() or abort_processing
unsubscribe channel, handle_subscription
res.end()
• If events are pending
• Or if there’s already a connection waiting for the same
channel
• Then return the event data immediately
• And tell the client when to reconnect
• The abort_processing mess is because of support for
both EventSource and long-polling
if waiting() or (evts and evts.length > 0)
abort_processing = write(res, evts, not waiting());
if waiting() or abort_processing
unsubscribe channel, handle_subscription
res.end()
• If events are pending
• Or if there’s already a connection waiting for the same
channel
• Then return the event data immediately
• And tell the client when to reconnect
• The abort_processing mess is because of support for
both EventSource and long-polling
if waiting() or (evts and evts.length > 0)
abort_processing = write(res, evts, not waiting());
if waiting() or abort_processing
unsubscribe channel, handle_subscription
res.end()
• If events are pending
• Or if there’s already a connection waiting for the same
channel
• Then return the event data immediately
• And tell the client when to reconnect
• The abort_processing mess is because of support for
both EventSource and long-polling
if waiting() or (evts and evts.length > 0)
abort_processing = write(res, evts, not waiting());
if waiting() or abort_processing
unsubscribe channel, handle_subscription
res.end()
• If events are pending
• Or if there’s already a connection waiting for the same
channel
• Then return the event data immediately
• And tell the client when to reconnect
• The abort_processing mess is because of support for
both EventSource and long-polling
handle_subscription = (c, message)->
fetch_events channel, last_event_id, (err, evts)->
return http_error 500, 'Failed to get event data' if err
abort_processing = write res, evts, true
last_event_id = evts[evts.length-1].id if (evts and evts.length > 0)
if abort_processing
unsubscribe channel, handle_subscription
clear_waiting()
res.end()
set_waiting()
subscribe channel, handle_subscription
Waiting
handle_subscription = (c, message)->
fetch_events channel, last_event_id, (err, evts)->
return http_error 500, 'Failed to get event data' if err
abort_processing = write res, evts, true
last_event_id = evts[evts.length-1].id if (evts and evts.length > 0)
if abort_processing
unsubscribe channel, handle_subscription
clear_waiting()
res.end()
set_waiting()
subscribe channel, handle_subscription
Waiting
handle_subscription = (c, message)->
fetch_events channel, last_event_id, (err, evts)->
return http_error 500, 'Failed to get event data' if err
abort_processing = write res, evts, true
last_event_id = evts[evts.length-1].id if (evts and evts.length > 0)
if abort_processing
unsubscribe channel, handle_subscription
clear_waiting()
res.end()
set_waiting()
subscribe channel, handle_subscription
Waiting
handle_subscription = (c, message)->
fetch_events channel, last_event_id, (err, evts)->
return http_error 500, 'Failed to get event data' if err
abort_processing = write res, evts, true
last_event_id = evts[evts.length-1].id if (evts and evts.length > 0)
if abort_processing
unsubscribe channel, handle_subscription
clear_waiting()
res.end()
set_waiting()
subscribe channel, handle_subscription
Waiting
handle_subscription = (c, message)->
fetch_events channel, last_event_id, (err, evts)->
return http_error 500, 'Failed to get event data' if err
abort_processing = write res, evts, true
last_event_id = evts[evts.length-1].id if (evts and evts.length > 0)
if abort_processing
unsubscribe channel, handle_subscription
clear_waiting()
res.end()
set_waiting()
subscribe channel, handle_subscription
Waiting
LOL - Boolean
parameter!!!
handle_subscription = (c, message)->
fetch_events channel, last_event_id, (err, evts)->
return http_error 500, 'Failed to get event data' if err
abort_processing = write res, evts, true
last_event_id = evts[evts.length-1].id if (evts and evts.length > 0)
if abort_processing
unsubscribe channel, handle_subscription
clear_waiting()
res.end()
set_waiting()
subscribe channel, handle_subscription
Waiting
Fallback
• Our fronted code connects to /e.php
• Our reverse proxy redirects that to the node
daemon
• If that daemon is down or no reverse proxy is there,
there’s an actual honest-to god /e.php …
• …which follows the exact same interface but is
always* short-polling
Client is more complicated
poll: =>
url = "#{@endpoint}/#{@channel}/#{@wait_id}"
$.ajax url,
cache: false,
dataType: 'json',
headers:
'Last-Event-Id': @last_event_id
success: (data, s, xhr) =>
return unless @enabled
@fireAll data
reconnect_in = parseInt xhr.getResponseHeader('x-ps-reconnect-in'), 10
reconnect_in = 10 unless reconnect_in >= 0
setTimeout @poll, reconnect_in*1000 if @enabled
error: (xhr, textStatus, error) =>
return unless @enabled
# 504 means nginx gave up waiting. This is totally to be
# expected and we can just treat it as an invitation to
# reconnect immediately. All other cases are likely bad, so
# we remove a bit of load by waiting a really long time
# 12002 is the ie proprietary way to report an WinInet timeout
# if it was registry-hacked to a low ReadTimeout.
# This isn't a server-error, so we can just reconnect.
rc = if (xhr.status in [504, 12002]) || (textStatus == 'timeout') then 0 else 10000
setTimeout @poll, rc if @enabled
Client is more complicated
poll: =>
url = "#{@endpoint}/#{@channel}/#{@wait_id}"
$.ajax url,
cache: false,
dataType: 'json',
headers:
'Last-Event-Id': @last_event_id
success: (data, s, xhr) =>
return unless @enabled
@fireAll data
reconnect_in = parseInt xhr.getResponseHeader('x-ps-reconnect-in'), 10
reconnect_in = 10 unless reconnect_in >= 0
setTimeout @poll, reconnect_in*1000 if @enabled
error: (xhr, textStatus, error) =>
return unless @enabled
# 504 means nginx gave up waiting. This is totally to be
# expected and we can just treat it as an invitation to
# reconnect immediately. All other cases are likely bad, so
# we remove a bit of load by waiting a really long time
# 12002 is the ie proprietary way to report an WinInet timeout
# if it was registry-hacked to a low ReadTimeout.
# This isn't a server-error, so we can just reconnect.
rc = if (xhr.status in [504, 12002]) || (textStatus == 'timeout') then 0 else 10000
setTimeout @poll, rc if @enabled
Client is more complicated
poll: =>
url = "#{@endpoint}/#{@channel}/#{@wait_id}"
$.ajax url,
cache: false,
dataType: 'json',
headers:
'Last-Event-Id': @last_event_id
success: (data, s, xhr) =>
return unless @enabled
@fireAll data
reconnect_in = parseInt xhr.getResponseHeader('x-ps-reconnect-in'), 10
reconnect_in = 10 unless reconnect_in >= 0
setTimeout @poll, reconnect_in*1000 if @enabled
error: (xhr, textStatus, error) =>
return unless @enabled
# 504 means nginx gave up waiting. This is totally to be
# expected and we can just treat it as an invitation to
# reconnect immediately. All other cases are likely bad, so
# we remove a bit of load by waiting a really long time
# 12002 is the ie proprietary way to report an WinInet timeout
# if it was registry-hacked to a low ReadTimeout.
# This isn't a server-error, so we can just reconnect.
rc = if (xhr.status in [504, 12002]) || (textStatus == 'timeout') then 0 else 10000
setTimeout @poll, rc if @enabled
Client is more complicated
poll: =>
url = "#{@endpoint}/#{@channel}/#{@wait_id}"
$.ajax url,
cache: false,
dataType: 'json',
headers:
'Last-Event-Id': @last_event_id
success: (data, s, xhr) =>
return unless @enabled
@fireAll data
reconnect_in = parseInt xhr.getResponseHeader('x-ps-reconnect-in'), 10
reconnect_in = 10 unless reconnect_in >= 0
setTimeout @poll, reconnect_in*1000 if @enabled
error: (xhr, textStatus, error) =>
return unless @enabled
# 504 means nginx gave up waiting. This is totally to be
# expected and we can just treat it as an invitation to
# reconnect immediately. All other cases are likely bad, so
# we remove a bit of load by waiting a really long time
# 12002 is the ie proprietary way to report an WinInet timeout
# if it was registry-hacked to a low ReadTimeout.
# This isn't a server-error, so we can just reconnect.
rc = if (xhr.status in [504, 12002]) || (textStatus == 'timeout') then 0 else 10000
setTimeout @poll, rc if @enabled
Client is more complicated
poll: =>
url = "#{@endpoint}/#{@channel}/#{@wait_id}"
$.ajax url,
cache: false,
dataType: 'json',
headers:
'Last-Event-Id': @last_event_id
success: (data, s, xhr) =>
return unless @enabled
@fireAll data
reconnect_in = parseInt xhr.getResponseHeader('x-ps-reconnect-in'), 10
reconnect_in = 10 unless reconnect_in >= 0
setTimeout @poll, reconnect_in*1000 if @enabled
error: (xhr, textStatus, error) =>
return unless @enabled
# 504 means nginx gave up waiting. This is totally to be
# expected and we can just treat it as an invitation to
# reconnect immediately. All other cases are likely bad, so
# we remove a bit of load by waiting a really long time
# 12002 is the ie proprietary way to report an WinInet timeout
# if it was registry-hacked to a low ReadTimeout.
# This isn't a server-error, so we can just reconnect.
rc = if (xhr.status in [504, 12002]) || (textStatus == 'timeout') then 0 else 10000
setTimeout @poll, rc if @enabled
Client is more complicated
poll: =>
url = "#{@endpoint}/#{@channel}/#{@wait_id}"
$.ajax url,
cache: false,
dataType: 'json',
headers:
'Last-Event-Id': @last_event_id
success: (data, s, xhr) =>
return unless @enabled
@fireAll data
reconnect_in = parseInt xhr.getResponseHeader('x-ps-reconnect-in'), 10
reconnect_in = 10 unless reconnect_in >= 0
setTimeout @poll, reconnect_in*1000 if @enabled
error: (xhr, textStatus, error) =>
return unless @enabled
# 504 means nginx gave up waiting. This is totally to be
# expected and we can just treat it as an invitation to
# reconnect immediately. All other cases are likely bad, so
# we remove a bit of load by waiting a really long time
# 12002 is the ie proprietary way to report an WinInet timeout
# if it was registry-hacked to a low ReadTimeout.
# This isn't a server-error, so we can just reconnect.
rc = if (xhr.status in [504, 12002]) || (textStatus == 'timeout') then 0 else 10000
setTimeout @poll, rc if @enabled
So.Why a daemon?
• Evented architecture lends itself well to many open
connections never really using CPU
• You do not want to long-poll with forking
architectures
• Unless you have unlimited RAM
In conclusionalso quite different from what I initially meant to say
https://twitter.com/pilif/status/491943226258239490
And that was last
wednesday
So…
• If your clients use browsers (and IE10+)
• If your clients use browsers (and IE10+)
• and if you have a good reverse proxy
• If your clients use browsers (and IE10+)
• and if you have a good reverse proxy
• and if you can use SSL
• If your clients use browsers (and IE10+)
• and if you have a good reverse proxy
• and if you can use SSL
• then use WebSockets
• If your clients use browsers (and IE10+)
• and if you have a good reverse proxy
• and if you can use SSL
• then use WebSockets
• Otherwise use long polling
• If your clients use browsers (and IE10+)
• and if you have a good reverse proxy
• and if you can use SSL
• then use WebSockets
• Otherwise use long polling
• Also, only use one - don’t mix - not worth the effort
• If your clients use browsers (and IE10+)
• and if you have a good reverse proxy
• and if you can use SSL
• then use WebSockets
• Otherwise use long polling
• Also, only use one - don’t mix - not worth the effort
• EventSource, frankly, sucks
Thank you!
• @pilif on twitter
• https://github.com/pilif/server-side-events

Also: We are looking for a front-end designer with CSS
skills and a backend developer. If you are interested or
know somebody, come to me

Server Side Events

  • 1.
    WHAT just happened? Reactingto Events on the Server
  • 2.
  • 3.
    • @pilif ontwitter • pilif on github • working at Sensational AG
  • 4.
    • @pilif ontwitter • pilif on github • working at Sensational AG • strongly dislike shirts
  • 5.
    "💩".length() is still2 Though there is a lot of good news with ES6
  • 6.
    There will besome ☕
  • 7.
  • 8.
    Server-Side Events • Informusers about stuff happening while they are using the site • Edits made to the current resource by other people • Chat Messages • Mobile Devices interacting with the Account
  • 9.
    Additional Constraints • Mustnot lose events • Events must be unique • Must work with shared sessions • Separate channels per user • Must work* even when hand-written daemons are down • Must work* in development without massaging daemons
  • 10.
    Not losing events •Race condition between event happening and infrastructure coming up on page load • Need to persist events • Using a database • Using a sequence (auto increment ID) to identify last sent event • Falling back to timestamps if not available (initial page load)
  • 11.
    But back tothe topic
  • 12.
    TIMTWWTDI * • Short Polling •Long Polling • EventSource • Web Sockets * yes. I’m that old
  • 13.
    Short Polling • Arewe there yet? • Are we there yet? • Are we there yet? • And now?
  • 14.
    Long Polling • Senda Query to the Server • Have the server only* reply when an event is available • Keep the connection open otherwise • Response means: event has happened • Have the client reconnect immediately
  • 15.
    Server-Sent Events • http://www.w3.org/TR/eventsource/ •Keeping a connection open to the server • Server is sending data as text/event-stream • Colon-separated key-value data. • Empty line separates events.
  • 16.
    WebSockets • «TCP/IP inyour browser»* • Full Duplex • Message passing • Persistent connection between Browser and Server
  • 17.
  • 18.
  • 19.
    index.html <script> (function(){ var channel =new EventChannel(); var log_ws = $('#websockets'); $(channel.bind('cheese_created', function(e){ log_ws.prepend($(‘<li>').text( e.pieces + ' pieces of ' + e.cheese_type )); }); })(); </script>
  • 20.
    index.html <script> (function(){ var channel =new EventChannel(); var log_ws = $('#websockets'); $(channel.bind('cheese_created', function(e){ log_ws.prepend($(‘<li>').text( e.pieces + ' pieces of ' + e.cheese_type )); }); })(); </script>
  • 21.
    index.html <script> (function(){ var channel =new EventChannel(); var log_ws = $('#websockets'); $(channel.bind('cheese_created', function(e){ log_ws.prepend($(‘<li>').text( e.pieces + ' pieces of ' + e.cheese_type )); }); })(); </script>
  • 22.
    index.html <script> (function(){ var channel =new EventChannel(); var log_ws = $('#websockets'); $(channel.bind('cheese_created', function(e){ log_ws.prepend($(‘<li>').text( e.pieces + ' pieces of ' + e.cheese_type )); }); })(); </script>
  • 23.
    publish.js • Creates between5 and 120 pieces of random Swiss cheese • Publishes an event about this • We’re using redis as our Pub/Sub mechanism, but you could use other solutions too • Sorry for the indentation, but code had to fit the slide var cheese_types = ['Emmentaler', 'Appenzeller', 'Gruyère', 'Vacherin', ‘Sprinz' ]; function create_cheese(){ return { pieces: Math.floor(Math.random() * 115) + 5, cheese_type: cheese_types[Math.floor( Math.random() *cheese_types.length )] } } var cheese_delivery = create_cheese(); publish(cheese_delivery);
  • 24.
  • 25.
    Server • Do nottry this at home • Use a library.You might know of socket.io – Me personally, I used ws. • Our code: only 32 lines.
  • 26.
    This is it varWebSocketServer = require('ws').Server; var redis = require('redis'); var wss = new WebSocketServer({port: 8080}); wss.on('connection', function(ws) { var client = redis.createClient(6379, 'localhost'); ws.on('close', function(){ client.end(); }); client.select(2, function(err, result){ if (err) { console.log("Failed to set redis database"); return; } client.subscribe('channels:cheese'); client.on('message', function(chn, message){ ws.send(message); }); }) });
  • 27.
    Actually, this isthe meat client.subscribe('channels:cheese'); client.on('message', function(chn, message){ ws.send(message); });
  • 28.
    And here’s theclient (function(window){ window.EventChannelWs = function(){ var socket = new WebSocket("ws://localhost:8080/"); var self = this; socket.onmessage = function(evt){ var event_info = JSON.parse(evt.data); var evt = jQuery.Event(event_info.type, event_info.data); $(self).trigger(evt); } } })(window);
  • 29.
    And here’s theclient (function(window){ window.EventChannelWs = function(){ var socket = new WebSocket("ws://localhost:8080/"); var self = this; socket.onmessage = function(evt){ var event_info = JSON.parse(evt.data); var evt = jQuery.Event(event_info.type, event_info.data); $(self).trigger(evt); } } })(window);
  • 30.
    And here’s theclient (function(window){ window.EventChannelWs = function(){ var socket = new WebSocket("ws://localhost:8080/"); var self = this; socket.onmessage = function(evt){ var event_info = JSON.parse(evt.data); var evt = jQuery.Event(event_info.type, event_info.data); $(self).trigger(evt); } } })(window);
  • 31.
    And here’s theclient (function(window){ window.EventChannelWs = function(){ var socket = new WebSocket("ws://localhost:8080/"); var self = this; socket.onmessage = function(evt){ var event_info = JSON.parse(evt.data); var evt = jQuery.Event(event_info.type, event_info.data); $(self).trigger(evt); } } })(window);
  • 32.
    Sample was verysimple • No synchronisation with server for initial event • No fallback when the web socket server is down • No reverse proxy involved • No channel separation
  • 33.
  • 35.
    PoweringYour 39 Lines •6K lines of JavaScript code • Plus 3.3K lines of C code • Plus 508 lines of C++ code • Which is the body that you actually run (excluding tests and benchmarks) • Some of which redundant because NPM
  • 36.
    WebSockets are abloody mess™ • RFC6455 is 71 pages long • Adding a lot of bit twiddling to intentionally break proxy servers • Proxies that work might only actually work* • Many deployments require a special port to run over
  • 37.
    Use SSL. By theGods. Use SSL
  • 38.
  • 39.
    Client var cheese_channel =new EventSource(url); var log_source = $('#eventsource'); cheese_channel.addEventListener('cheese_created', function(e){ var data = JSON.parse(e.data); log_source.prepend($(‘<li>').text( data.pieces + ' pieces of ' + data.cheese_type )); });
  • 40.
    Client var cheese_channel =new EventSource(url); var log_source = $('#eventsource'); cheese_channel.addEventListener('cheese_created', function(e){ var data = JSON.parse(e.data); log_source.prepend($(‘<li>').text( data.pieces + ' pieces of ' + data.cheese_type )); });
  • 41.
    Client var cheese_channel =new EventSource(url); var log_source = $('#eventsource'); cheese_channel.addEventListener('cheese_created', function(e){ var data = JSON.parse(e.data); log_source.prepend($(‘<li>').text( data.pieces + ' pieces of ' + data.cheese_type )); });
  • 42.
    Client var cheese_channel =new EventSource(url); var log_source = $('#eventsource'); cheese_channel.addEventListener('cheese_created', function(e){ var data = JSON.parse(e.data); log_source.prepend($(‘<li>').text( data.pieces + ' pieces of ' + data.cheese_type )); });
  • 43.
    Server • Keeps theconnection open • Sends blank-line separated groups of key/value pairs as events happen • Can tell the client how long to wait when reconnecting
  • 44.
    Disillusioning • Bound tothe 6-connections per host rule • Still needs manual synchronising if you don’t want to lose events • Browser support is as always
  • 45.
    Disillusioning • Bound tothe 6-connections per host rule • Still needs manual synchronising if you don’t want to lose events • Browser support is as always
  • 46.
  • 47.
    I like it •Works even with IE6 (god forbid you have to do this) • Works fine with proxies • On both ends • Works fine over HTTP • Needs some help due to the connection limit • Works even when your infrastructure is down*
  • 48.
    Production code • Thefollowing code samples form the basis of the initial demo • It’s production code • No support issue caused by this. • Runs fine in a developer-hostile environment
  • 49.
  • 50.
    Synchronising using the database events_since_id= (channel, id, cb)-> q = """ select * from events where channel_id = $1 and id > $2 order by id asc """ query q, [channel, id], cb events_since_time = (channel, ts, cb)-> q = """ select * from events o where channel_id = $1 and ts > (SELECT TIMESTAMP WITH TIME ZONE 'epoch' + $2 * INTERVAL '1 second’ ) order by id asc """ query q, [channel, ts], cb
  • 51.
    The meat handle_subscription =(c, message)-> fetch_events channel, last_event_id, (err, evts)-> return http_error 500, 'Failed to get event data' if err abort_processing = write res, evts, true last_event_id = evts[evts.length-1].id if (evts and evts.length > 0) if abort_processing unsubscribe channel, handle_subscription clear_waiting() res.end() fetch_events channel, last_event_id, (err, evts)-> return http_error res, 500, 'Failed to get event data: ' + err if err last_event_id = evts[evts.length-1].id if (evts and evts.length > 0) if waiting() or (evts and evts.length > 0) abort_processing = write(res, evts, not waiting()); if waiting() or abort_processing unsubscribe channel, handle_subscription res.end() set_waiting() subscribe channel, handle_subscription
  • 52.
    if waiting() or(evts and evts.length > 0) abort_processing = write(res, evts, not waiting()); if waiting() or abort_processing unsubscribe channel, handle_subscription res.end() • If events are pending • Or if there’s already a connection waiting for the same channel • Then return the event data immediately • And tell the client when to reconnect • The abort_processing mess is because of support for both EventSource and long-polling
  • 53.
    if waiting() or(evts and evts.length > 0) abort_processing = write(res, evts, not waiting()); if waiting() or abort_processing unsubscribe channel, handle_subscription res.end() • If events are pending • Or if there’s already a connection waiting for the same channel • Then return the event data immediately • And tell the client when to reconnect • The abort_processing mess is because of support for both EventSource and long-polling
  • 54.
    if waiting() or(evts and evts.length > 0) abort_processing = write(res, evts, not waiting()); if waiting() or abort_processing unsubscribe channel, handle_subscription res.end() • If events are pending • Or if there’s already a connection waiting for the same channel • Then return the event data immediately • And tell the client when to reconnect • The abort_processing mess is because of support for both EventSource and long-polling
  • 55.
    if waiting() or(evts and evts.length > 0) abort_processing = write(res, evts, not waiting()); if waiting() or abort_processing unsubscribe channel, handle_subscription res.end() • If events are pending • Or if there’s already a connection waiting for the same channel • Then return the event data immediately • And tell the client when to reconnect • The abort_processing mess is because of support for both EventSource and long-polling
  • 56.
    if waiting() or(evts and evts.length > 0) abort_processing = write(res, evts, not waiting()); if waiting() or abort_processing unsubscribe channel, handle_subscription res.end() • If events are pending • Or if there’s already a connection waiting for the same channel • Then return the event data immediately • And tell the client when to reconnect • The abort_processing mess is because of support for both EventSource and long-polling
  • 57.
    handle_subscription = (c,message)-> fetch_events channel, last_event_id, (err, evts)-> return http_error 500, 'Failed to get event data' if err abort_processing = write res, evts, true last_event_id = evts[evts.length-1].id if (evts and evts.length > 0) if abort_processing unsubscribe channel, handle_subscription clear_waiting() res.end() set_waiting() subscribe channel, handle_subscription Waiting
  • 58.
    handle_subscription = (c,message)-> fetch_events channel, last_event_id, (err, evts)-> return http_error 500, 'Failed to get event data' if err abort_processing = write res, evts, true last_event_id = evts[evts.length-1].id if (evts and evts.length > 0) if abort_processing unsubscribe channel, handle_subscription clear_waiting() res.end() set_waiting() subscribe channel, handle_subscription Waiting
  • 59.
    handle_subscription = (c,message)-> fetch_events channel, last_event_id, (err, evts)-> return http_error 500, 'Failed to get event data' if err abort_processing = write res, evts, true last_event_id = evts[evts.length-1].id if (evts and evts.length > 0) if abort_processing unsubscribe channel, handle_subscription clear_waiting() res.end() set_waiting() subscribe channel, handle_subscription Waiting
  • 60.
    handle_subscription = (c,message)-> fetch_events channel, last_event_id, (err, evts)-> return http_error 500, 'Failed to get event data' if err abort_processing = write res, evts, true last_event_id = evts[evts.length-1].id if (evts and evts.length > 0) if abort_processing unsubscribe channel, handle_subscription clear_waiting() res.end() set_waiting() subscribe channel, handle_subscription Waiting
  • 61.
    handle_subscription = (c,message)-> fetch_events channel, last_event_id, (err, evts)-> return http_error 500, 'Failed to get event data' if err abort_processing = write res, evts, true last_event_id = evts[evts.length-1].id if (evts and evts.length > 0) if abort_processing unsubscribe channel, handle_subscription clear_waiting() res.end() set_waiting() subscribe channel, handle_subscription Waiting LOL - Boolean parameter!!!
  • 62.
    handle_subscription = (c,message)-> fetch_events channel, last_event_id, (err, evts)-> return http_error 500, 'Failed to get event data' if err abort_processing = write res, evts, true last_event_id = evts[evts.length-1].id if (evts and evts.length > 0) if abort_processing unsubscribe channel, handle_subscription clear_waiting() res.end() set_waiting() subscribe channel, handle_subscription Waiting
  • 63.
    Fallback • Our frontedcode connects to /e.php • Our reverse proxy redirects that to the node daemon • If that daemon is down or no reverse proxy is there, there’s an actual honest-to god /e.php … • …which follows the exact same interface but is always* short-polling
  • 64.
    Client is morecomplicated poll: => url = "#{@endpoint}/#{@channel}/#{@wait_id}" $.ajax url, cache: false, dataType: 'json', headers: 'Last-Event-Id': @last_event_id success: (data, s, xhr) => return unless @enabled @fireAll data reconnect_in = parseInt xhr.getResponseHeader('x-ps-reconnect-in'), 10 reconnect_in = 10 unless reconnect_in >= 0 setTimeout @poll, reconnect_in*1000 if @enabled error: (xhr, textStatus, error) => return unless @enabled # 504 means nginx gave up waiting. This is totally to be # expected and we can just treat it as an invitation to # reconnect immediately. All other cases are likely bad, so # we remove a bit of load by waiting a really long time # 12002 is the ie proprietary way to report an WinInet timeout # if it was registry-hacked to a low ReadTimeout. # This isn't a server-error, so we can just reconnect. rc = if (xhr.status in [504, 12002]) || (textStatus == 'timeout') then 0 else 10000 setTimeout @poll, rc if @enabled
  • 65.
    Client is morecomplicated poll: => url = "#{@endpoint}/#{@channel}/#{@wait_id}" $.ajax url, cache: false, dataType: 'json', headers: 'Last-Event-Id': @last_event_id success: (data, s, xhr) => return unless @enabled @fireAll data reconnect_in = parseInt xhr.getResponseHeader('x-ps-reconnect-in'), 10 reconnect_in = 10 unless reconnect_in >= 0 setTimeout @poll, reconnect_in*1000 if @enabled error: (xhr, textStatus, error) => return unless @enabled # 504 means nginx gave up waiting. This is totally to be # expected and we can just treat it as an invitation to # reconnect immediately. All other cases are likely bad, so # we remove a bit of load by waiting a really long time # 12002 is the ie proprietary way to report an WinInet timeout # if it was registry-hacked to a low ReadTimeout. # This isn't a server-error, so we can just reconnect. rc = if (xhr.status in [504, 12002]) || (textStatus == 'timeout') then 0 else 10000 setTimeout @poll, rc if @enabled
  • 66.
    Client is morecomplicated poll: => url = "#{@endpoint}/#{@channel}/#{@wait_id}" $.ajax url, cache: false, dataType: 'json', headers: 'Last-Event-Id': @last_event_id success: (data, s, xhr) => return unless @enabled @fireAll data reconnect_in = parseInt xhr.getResponseHeader('x-ps-reconnect-in'), 10 reconnect_in = 10 unless reconnect_in >= 0 setTimeout @poll, reconnect_in*1000 if @enabled error: (xhr, textStatus, error) => return unless @enabled # 504 means nginx gave up waiting. This is totally to be # expected and we can just treat it as an invitation to # reconnect immediately. All other cases are likely bad, so # we remove a bit of load by waiting a really long time # 12002 is the ie proprietary way to report an WinInet timeout # if it was registry-hacked to a low ReadTimeout. # This isn't a server-error, so we can just reconnect. rc = if (xhr.status in [504, 12002]) || (textStatus == 'timeout') then 0 else 10000 setTimeout @poll, rc if @enabled
  • 67.
    Client is morecomplicated poll: => url = "#{@endpoint}/#{@channel}/#{@wait_id}" $.ajax url, cache: false, dataType: 'json', headers: 'Last-Event-Id': @last_event_id success: (data, s, xhr) => return unless @enabled @fireAll data reconnect_in = parseInt xhr.getResponseHeader('x-ps-reconnect-in'), 10 reconnect_in = 10 unless reconnect_in >= 0 setTimeout @poll, reconnect_in*1000 if @enabled error: (xhr, textStatus, error) => return unless @enabled # 504 means nginx gave up waiting. This is totally to be # expected and we can just treat it as an invitation to # reconnect immediately. All other cases are likely bad, so # we remove a bit of load by waiting a really long time # 12002 is the ie proprietary way to report an WinInet timeout # if it was registry-hacked to a low ReadTimeout. # This isn't a server-error, so we can just reconnect. rc = if (xhr.status in [504, 12002]) || (textStatus == 'timeout') then 0 else 10000 setTimeout @poll, rc if @enabled
  • 68.
    Client is morecomplicated poll: => url = "#{@endpoint}/#{@channel}/#{@wait_id}" $.ajax url, cache: false, dataType: 'json', headers: 'Last-Event-Id': @last_event_id success: (data, s, xhr) => return unless @enabled @fireAll data reconnect_in = parseInt xhr.getResponseHeader('x-ps-reconnect-in'), 10 reconnect_in = 10 unless reconnect_in >= 0 setTimeout @poll, reconnect_in*1000 if @enabled error: (xhr, textStatus, error) => return unless @enabled # 504 means nginx gave up waiting. This is totally to be # expected and we can just treat it as an invitation to # reconnect immediately. All other cases are likely bad, so # we remove a bit of load by waiting a really long time # 12002 is the ie proprietary way to report an WinInet timeout # if it was registry-hacked to a low ReadTimeout. # This isn't a server-error, so we can just reconnect. rc = if (xhr.status in [504, 12002]) || (textStatus == 'timeout') then 0 else 10000 setTimeout @poll, rc if @enabled
  • 69.
    Client is morecomplicated poll: => url = "#{@endpoint}/#{@channel}/#{@wait_id}" $.ajax url, cache: false, dataType: 'json', headers: 'Last-Event-Id': @last_event_id success: (data, s, xhr) => return unless @enabled @fireAll data reconnect_in = parseInt xhr.getResponseHeader('x-ps-reconnect-in'), 10 reconnect_in = 10 unless reconnect_in >= 0 setTimeout @poll, reconnect_in*1000 if @enabled error: (xhr, textStatus, error) => return unless @enabled # 504 means nginx gave up waiting. This is totally to be # expected and we can just treat it as an invitation to # reconnect immediately. All other cases are likely bad, so # we remove a bit of load by waiting a really long time # 12002 is the ie proprietary way to report an WinInet timeout # if it was registry-hacked to a low ReadTimeout. # This isn't a server-error, so we can just reconnect. rc = if (xhr.status in [504, 12002]) || (textStatus == 'timeout') then 0 else 10000 setTimeout @poll, rc if @enabled
  • 70.
    So.Why a daemon? •Evented architecture lends itself well to many open connections never really using CPU • You do not want to long-poll with forking architectures • Unless you have unlimited RAM
  • 71.
    In conclusionalso quitedifferent from what I initially meant to say
  • 72.
  • 73.
    And that waslast wednesday
  • 75.
  • 77.
    • If yourclients use browsers (and IE10+)
  • 78.
    • If yourclients use browsers (and IE10+) • and if you have a good reverse proxy
  • 79.
    • If yourclients use browsers (and IE10+) • and if you have a good reverse proxy • and if you can use SSL
  • 80.
    • If yourclients use browsers (and IE10+) • and if you have a good reverse proxy • and if you can use SSL • then use WebSockets
  • 81.
    • If yourclients use browsers (and IE10+) • and if you have a good reverse proxy • and if you can use SSL • then use WebSockets • Otherwise use long polling
  • 82.
    • If yourclients use browsers (and IE10+) • and if you have a good reverse proxy • and if you can use SSL • then use WebSockets • Otherwise use long polling • Also, only use one - don’t mix - not worth the effort
  • 83.
    • If yourclients use browsers (and IE10+) • and if you have a good reverse proxy • and if you can use SSL • then use WebSockets • Otherwise use long polling • Also, only use one - don’t mix - not worth the effort • EventSource, frankly, sucks
  • 84.
    Thank you! • @pilifon twitter • https://github.com/pilif/server-side-events
 Also: We are looking for a front-end designer with CSS skills and a backend developer. If you are interested or know somebody, come to me