Live Streaming & Server Sent Events

8,003 views

Published on

Published in: Technology

Live Streaming & Server Sent Events

  1. 1. Live Streaming &Server Sent Events Tomáš Kramár @tkramar
  2. 2. 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
  3. 3. How? / Terminology● AJAX polling● Comet● WebSockets● Server-Sent Events
  4. 4. AJAX polling Any news?Browser/Client Server
  5. 5. AJAX polling Any news? NoBrowser/Client Server
  6. 6. AJAX polling Any news? No Any news? NoBrowser/Client Server
  7. 7. AJAX polling Any news? No Any news? No Any news?Browser/Client Yes! Server
  8. 8. AJAX polling Any news? No Any news? No Any news?Browser/Client Yes! Server Any news? No
  9. 9. AJAX polling● Overhead – Establishing new connections, TCP handshakes – Sending HTTP headers – Multiply by number of clients● Not really realtime – Poll each 2 seconds
  10. 10. Comet● set of technology principles/communication patterns● mostly hacks – forever-iframe – htmlfile ActiveX object – XHR multipart/streaming/long-polling – Flash – ..
  11. 11. WebSockets● bi-directional, full-duplex communication channels over a single TCP connection● HTML5● being standardized
  12. 12. Server-Sent Events● HTML5● Traditional HTTP – No special protocol or server implementation● Browser establishes single connection and waits● Server generates events
  13. 13. SSE Request w parameters id: 1 event: display data: { foo: moo }Browser/Client Server
  14. 14. SSE Request w parameters id: 1 event: display data: { foo: moo }Browser/Client id: 2 Server event: redraw data: { boo: hoo }
  15. 15. 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 ● ... – …● Dont let the user wait, display results when they are available
  16. 16. Demo
  17. 17. Clientthis.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));});
  18. 18. Client gotchas ● Special events: – open – error ● Dont forget to close the requestself.source.addEventListener(finished, function(e) { self.status.searchFinished(); self.source.close();});
  19. 19. Server● Must support – long-running request – Live-streaming (i.e., no output buffering)● Rainbows!, Puma or Thin● Rails 4 (beta) supports live streaming
  20. 20. Rails 4 Live Streamingclass 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
  21. 21. require jsonclass SSE def initialize io event: displayn @io = io data: { foo: moo }nn end def write object, options = {} options.each do |k,v| @io.write "#{k}: #{v}n" end @io.write "data: #{JSON.dump(object)}nn" end def close @io.close endend
  22. 22. 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?
  23. 23. 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 dont need it if we remove the processed events
  24. 24. marks/search?q=eset GirlFriday Search 3342345 HTTP 202 Accepted marks/results?job_id=3342345Browser Server Redis marks/results?job_id=3342345 MarksController event: results data: {foo: boo} event: status data: {moo: hoo}
  25. 25. 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
  26. 26. class MarksController < ApplicationController include ActionController::Live def search! uuid = UUID.new.generate(:compact) TORT_QUEUE << { phrase: params[:q], job_id: uuid } generate job_id 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
  27. 27. class MarksController < ApplicationController include ActionController::Live def search! uuid = UUID.new.generate(:compact) TORT_QUEUE << { phrase: params[:q], job_id: uuid } start async job (GirlFriday) 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
  28. 28. 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 send results URL 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
  29. 29. 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) Get queue for this job, finished = false async job is pushing begin to this queue 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
  30. 30. 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| Fetch next message message = JSON.parse(json_message) from queue (blocks until case message["type"] when "results" then one is available) 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
  31. 31. 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 IOError is raised when client ensure sse.close disconnected and we are end end writing to response.streamend
  32. 32. GirlFriday workerclass 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
  33. 33. 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
  34. 34. EventSource Compatibility● Firefox 6+, Chrome 6+, Safari 5+, Opera 11+, iOS Safari 4+, Blackberry, Opera Mobile, Chrome for Android, Firefox for Android
  35. 35. 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
  36. 36. Summary● Unidirectional server-to-client communication● Single request● Real-time● Easy to implement● Well supported except for IE

×