Live Streaming & Server Sent Events
-
Upload
tkramar -
Category
Technology
-
view
6.226 -
download
0
Transcript of Live Streaming & Server Sent Events
Live Streaming &
Server Sent Events
Tomáš Kramár@tkramar
When?
● Server needs to stream data to client– Server decides when and what to send
– Client waits and listens
– Client does not need to send messages
– Uni-directional communication
– Asynchronously
How? / Terminology
● AJAX polling● Comet● WebSockets● Server-Sent Events
AJAX polling
Browser/Client Server
Any news?
AJAX polling
Browser/Client Server
Any news?
No
AJAX polling
Browser/Client Server
Any news?
No
Any news?
No
AJAX polling
Browser/Client Server
Any news?
No
Any news?
No
Any news?
Yes!
AJAX polling
Browser/Client Server
Any news?
No
Any news?
No
Any news?
Yes!
Any news?
No
AJAX polling
● Overhead– Establishing new connections, TCP handshakes
– Sending HTTP headers
– Multiply by number of clients
● Not really realtime– Poll each 2 seconds
Comet
● set of technology principles/communication patterns
● mostly hacks– forever-iframe
– htmlfile ActiveX object
– XHR multipart/streaming/long-polling
– Flash
– ..
WebSockets
● bi-directional, full-duplex communication channels over a single TCP connection
● HTML5● being standardized
Server-Sent Events
● HTML5● Traditional HTTP
– No special protocol or server implementation
● Browser establishes single connection and waits
● Server generates events
SSE
Browser/Client Server
Request \w parameters
id: 1event: displaydata: { foo: 'moo' }
SSE
Browser/Client Server
Request \w parameters
id: 1event: displaydata: { foo: 'moo' }
id: 2event: redrawdata: { boo: 'hoo' }
Case study
● Live search in trademark databases● query
– search in register #1● Search (~15s), parse search result list, fetch each result
(~3s each), go to next page in search result list (~10s), fetch each result, ...
– search in register #2● ...
– …
● Don't let the user wait, display results when they are available
Demo
Client
this.source = new EventSource('marks/search');
self.source.addEventListener('results', function(e) { self.marks.appendMarks($.parseJSON(e.data));});
self.source.addEventListener('failure', function(e) { self.errors.showError();});
self.source.addEventListener('status', function(e) { self.paging.update($.parseJSON(e.data));});
Client gotchas
● Special events:– open– error
● Don't forget to close the request
self.source.addEventListener('finished', function(e) {
self.status.searchFinished();
self.source.close();
});
Server
● Must support – long-running request
– Live-streaming (i.e., no output buffering)
● Rainbows!, Puma or Thin● Rails 4 (beta) supports live streaming
Rails 4 Live Streaming
class MarksController < ApplicationController include ActionController::Live
def results response.headers['Content-Type'] = 'text/event-stream' sse = SSE.new(response.stream)
Tort.search(params[:query]) do |on| on.results do |hits| sse.write(hits, event: 'result') end on.status_change do |status| sse.write(status, event: 'status') end on.error do sse.write({}, event: 'failure') end end endend
require 'json'
class SSE
def initialize io
@io = io
end
def write object, options = {}
options.each do |k,v|
@io.write "#{k}: #{v}\n"
end
@io.write "data: #{JSON.dump(object)}\n\n"
end
def close
@io.close
end
end
event: display\ndata: { foo: 'moo' }\n\n
Timeouts, lost connections, internet explorers and other bad things
● EventSource request can be interrupted● EventSource will reconnect automatically● What happens with the data during the time
connection was not available?
Handling reconnections
● When EventSource reconnects, we need to continue sending the data from the point the connection was lost– Do the work in the background and store events
somewhere
– In the controller, load events from the storage
● EventSource sends Last-Event-Id in HTTP header– But we don't need it if we remove the processed
events
Browser Server
marks/search?q=eset
HTTP 202 Acceptedmarks/results?job_id=3342345
GirlFridaySearch
3342345
Redis
marks/results?job_id=3342345
MarksController
event: resultsdata: {foo: 'boo'}
event: statusdata: {moo: 'hoo'}
class MarksController < ApplicationController include ActionController::Live
def search! uuid = UUID.new.generate(:compact) TORT_QUEUE << { phrase: params[:q], job_id: uuid }
render status: 202, text: marks_results_path(job: uuid) end
def results response.headers['Content-Type'] = 'text/event-stream' sse = SSE.new(response.stream) queue = SafeQueue.new(Channel.for_job(params[:job]), Tmzone.redis) finished = false
begin begin queue.next_message do |json_message| message = JSON.parse(json_message) case message["type"] when "results" then sse.write(message["data"], event: 'results') when "failure" then sse.write({}, event: 'failure') when "fatal" then sse.write({}, event: 'fatal') finished = true when "status" then sse.write(message["data"], event: 'status') when "finished" then sse.write({}, event: 'finished') finished = true end end end while !finished rescue IOError # when clients disconnects ensure sse.close end endend
class MarksController < ApplicationController include ActionController::Live
def search! uuid = UUID.new.generate(:compact) TORT_QUEUE << { phrase: params[:q], job_id: uuid }
render status: 202, text: marks_results_path(job: uuid) end
def results response.headers['Content-Type'] = 'text/event-stream' sse = SSE.new(response.stream) queue = SafeQueue.new(Channel.for_job(params[:job]), Tmzone.redis) finished = false
begin begin queue.next_message do |json_message| message = JSON.parse(json_message) case message["type"] when "results" then sse.write(message["data"], event: 'results') when "failure" then sse.write({}, event: 'failure') when "fatal" then sse.write({}, event: 'fatal') finished = true when "status" then sse.write(message["data"], event: 'status') when "finished" then sse.write({}, event: 'finished') finished = true end end end while !finished rescue IOError # when clients disconnects ensure sse.close end endend
generate job_id
class MarksController < ApplicationController include ActionController::Live
def search! uuid = UUID.new.generate(:compact) TORT_QUEUE << { phrase: params[:q], job_id: uuid }
render status: 202, text: marks_results_path(job: uuid) end
def results response.headers['Content-Type'] = 'text/event-stream' sse = SSE.new(response.stream) queue = SafeQueue.new(Channel.for_job(params[:job]), Tmzone.redis) finished = false
begin begin queue.next_message do |json_message| message = JSON.parse(json_message) case message["type"] when "results" then sse.write(message["data"], event: 'results') when "failure" then sse.write({}, event: 'failure') when "fatal" then sse.write({}, event: 'fatal') finished = true when "status" then sse.write(message["data"], event: 'status') when "finished" then sse.write({}, event: 'finished') finished = true end end end while !finished rescue IOError # when clients disconnects ensure sse.close end endend
start async job (GirlFriday)
class MarksController < ApplicationController include ActionController::Live
def search! uuid = UUID.new.generate(:compact) TORT_QUEUE << { phrase: params[:q], job_id: uuid }
render status: 202, text: marks_results_path(job: uuid) end
def results response.headers['Content-Type'] = 'text/event-stream' sse = SSE.new(response.stream) queue = SafeQueue.new(Channel.for_job(params[:job]), Tmzone.redis) finished = false
begin begin queue.next_message do |json_message| message = JSON.parse(json_message) case message["type"] when "results" then sse.write(message["data"], event: 'results') when "failure" then sse.write({}, event: 'failure') when "fatal" then sse.write({}, event: 'fatal') finished = true when "status" then sse.write(message["data"], event: 'status') when "finished" then sse.write({}, event: 'finished') finished = true end end end while !finished rescue IOError # when clients disconnects ensure sse.close end endend
send results URL
class MarksController < ApplicationController include ActionController::Live
def search! uuid = UUID.new.generate(:compact) TORT_QUEUE << { phrase: params[:q], job_id: uuid }
render status: 202, text: marks_results_path(job: uuid) end
def results response.headers['Content-Type'] = 'text/event-stream' sse = SSE.new(response.stream) queue = SafeQueue.new(Channel.for_job(params[:job]), Tmzone.redis) finished = false
begin begin queue.next_message do |json_message| message = JSON.parse(json_message) case message["type"] when "results" then sse.write(message["data"], event: 'results') when "failure" then sse.write({}, event: 'failure') when "fatal" then sse.write({}, event: 'fatal') finished = true when "status" then sse.write(message["data"], event: 'status') when "finished" then sse.write({}, event: 'finished') finished = true end end end while !finished rescue IOError # when clients disconnects ensure sse.close end endend
Get queue for this job, async job is pushing
to this queue
class MarksController < ApplicationController include ActionController::Live
def search! uuid = UUID.new.generate(:compact) TORT_QUEUE << { phrase: params[:q], job_id: uuid }
render status: 202, text: marks_results_path(job: uuid) end
def results response.headers['Content-Type'] = 'text/event-stream' sse = SSE.new(response.stream) queue = SafeQueue.new(Channel.for_job(params[:job]), Tmzone.redis) finished = false
begin begin queue.next_message do |json_message| message = JSON.parse(json_message) case message["type"] when "results" then sse.write(message["data"], event: 'results') when "failure" then sse.write({}, event: 'failure') when "fatal" then sse.write({}, event: 'fatal') finished = true when "status" then sse.write(message["data"], event: 'status') when "finished" then sse.write({}, event: 'finished') finished = true end end end while !finished rescue IOError # when clients disconnects ensure sse.close end endend
Fetch next message from queue (blocks until
one is available)
class MarksController < ApplicationController include ActionController::Live
def search! uuid = UUID.new.generate(:compact) TORT_QUEUE << { phrase: params[:q], job_id: uuid }
render status: 202, text: marks_results_path(job: uuid) end
def results response.headers['Content-Type'] = 'text/event-stream' sse = SSE.new(response.stream) queue = SafeQueue.new(Channel.for_job(params[:job]), Tmzone.redis) finished = false
begin begin queue.next_message do |json_message| message = JSON.parse(json_message) case message["type"] when "results" then sse.write(message["data"], event: 'results') when "failure" then sse.write({}, event: 'failure') when "fatal" then sse.write({}, event: 'fatal') finished = true when "status" then sse.write(message["data"], event: 'status') when "finished" then sse.write({}, event: 'finished') finished = true end end end while !finished rescue IOError # when clients disconnects ensure sse.close end endend
IOError is raised when client disconnected and we are writing to response.stream
GirlFriday worker
class SearchWorker def self.perform(phrase, job_id) channel = Channel.for_job(job_id) queue = SafeQueue.new(channel, Tmzone.redis)
Tort.search(phrase) do |on| on.results do |hits| queue.push({ type: "results", data: hits }.to_json) end on.status_change do |status| queue.push({ type: "status", data: status }.to_json) end on.error do queue.push({ type: 'failure' }.to_json) end end queue.push({ type: "finished" }.to_json) endend
SafeQueueclass SafeQueue def initialize(channel, redis) @channel = channel @redis = redis end
def next_message(&block) begin _, message = @redis.blpop(@channel) block.call(message) rescue => error @redis.lpush(@channel, message) raise error end end
def push(message) @redis.rpush(@channel, message) endend
EventSource Compatibility
● Firefox 6+, Chrome 6+, Safari 5+, Opera 11+, iOS Safari 4+, Blackberry, Opera Mobile, Chrome for Android, Firefox for Android
Fallback
● Polyfills– https://github.com/remy/polyfills/blob/master/Event
Source.js ● Hanging GET, waits until the request terminates,
essentially buffering the live output
– https://github.com/Yaffle/EventSource ● send a keep-alive message each 15 seconds
Summary
● Unidirectional server-to-client communication● Single request● Real-time● Easy to implement● Well supported except for IE