36
Live Streaming & Server Sent Events Tomáš Kramár @tkramar

Live Streaming & Server Sent Events

  • Upload
    tkramar

  • View
    6.215

  • Download
    0

Embed Size (px)

Citation preview

Page 1: Live Streaming & Server Sent Events

Live Streaming &

Server Sent Events

Tomáš Kramár@tkramar

Page 2: Live Streaming & Server Sent Events

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

Page 3: Live Streaming & Server Sent Events

How? / Terminology

● AJAX polling● Comet● WebSockets● Server-Sent Events

Page 4: Live Streaming & Server Sent Events

AJAX polling

Browser/Client Server

Any news?

Page 5: Live Streaming & Server Sent Events

AJAX polling

Browser/Client Server

Any news?

No

Page 6: Live Streaming & Server Sent Events

AJAX polling

Browser/Client Server

Any news?

No

Any news?

No

Page 7: Live Streaming & Server Sent Events

AJAX polling

Browser/Client Server

Any news?

No

Any news?

No

Any news?

Yes!

Page 8: Live Streaming & Server Sent Events

AJAX polling

Browser/Client Server

Any news?

No

Any news?

No

Any news?

Yes!

Any news?

No

Page 9: Live Streaming & Server Sent Events

AJAX polling

● Overhead– Establishing new connections, TCP handshakes

– Sending HTTP headers

– Multiply by number of clients

● Not really realtime– Poll each 2 seconds

Page 10: Live Streaming & Server Sent Events

Comet

● set of technology principles/communication patterns

● mostly hacks– forever-iframe

– htmlfile ActiveX object

– XHR multipart/streaming/long-polling

– Flash

– ..

Page 11: Live Streaming & Server Sent Events

WebSockets

● bi-directional, full-duplex communication channels over a single TCP connection

● HTML5● being standardized

Page 12: Live Streaming & Server Sent Events

Server-Sent Events

● HTML5● Traditional HTTP

– No special protocol or server implementation

● Browser establishes single connection and waits

● Server generates events

Page 13: Live Streaming & Server Sent Events

SSE

Browser/Client Server

Request \w parameters

id: 1event: displaydata: { foo: 'moo' }

Page 14: Live Streaming & Server Sent Events

SSE

Browser/Client Server

Request \w parameters

id: 1event: displaydata: { foo: 'moo' }

id: 2event: redrawdata: { boo: 'hoo' }

Page 15: Live Streaming & Server Sent Events

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

Page 16: Live Streaming & Server Sent Events

Demo

Page 17: Live Streaming & Server Sent Events

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));});

Page 18: Live Streaming & Server Sent Events

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();

});

Page 19: Live Streaming & Server Sent Events

Server

● Must support – long-running request

– Live-streaming (i.e., no output buffering)

● Rainbows!, Puma or Thin● Rails 4 (beta) supports live streaming

Page 20: Live Streaming & Server Sent Events

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

Page 21: Live Streaming & Server Sent Events

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

Page 22: Live Streaming & Server Sent Events

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?

Page 23: Live Streaming & Server Sent Events

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

Page 24: Live Streaming & Server Sent 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'}

Page 25: Live Streaming & Server Sent Events

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

Page 26: Live Streaming & Server Sent Events

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

Page 27: Live Streaming & Server Sent Events

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)

Page 28: Live Streaming & Server Sent Events

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

Page 29: Live Streaming & Server Sent Events

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

Page 30: Live Streaming & Server Sent Events

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)

Page 31: Live Streaming & Server Sent Events

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

Page 32: Live Streaming & Server Sent Events

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

Page 33: Live Streaming & Server Sent Events

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

Page 34: Live Streaming & Server Sent Events

EventSource Compatibility

● Firefox 6+, Chrome 6+, Safari 5+, Opera 11+, iOS Safari 4+, Blackberry, Opera Mobile, Chrome for Android, Firefox for Android

Page 35: Live Streaming & Server Sent Events

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

Page 36: Live Streaming & Server Sent Events

Summary

● Unidirectional server-to-client communication● Single request● Real-time● Easy to implement● Well supported except for IE