Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Chat App with Angelo and WebSockets in 15m

Chat App with Angelo and WebSockets in 15m

talk given at Cascadia Ruby 2014 about websockets, sse, ruby, and a gem called angelo that makes it super easy to work with these things.

Avatar for Kenichi Nakamura

Kenichi Nakamura

August 12, 2014

Other Decks in Technology

Transcript

  1. How to build a Blog Engine in 15 Minutes with

    Ruby on Rails http://rubyonrails.org By Kenichi Nakamura & Terence Lee
  2. How to build a Blog Engine Social App in 15

    Minutes with Ruby on Rails Angelo https://rubyonrails.org http://github.com/kenichi/angelo By Kenichi Nakamura & Terence Lee "Whoops!"
  3. Agenda WebSockets Reactor Pattern Server Sent Events Bonus Round Profit

    Ken Terence Ruby Karaoke Friday Hug ps://www.flickr.com/photos/turbojoe/556776940
  4. Agenda WebSockets Reactor Pattern Server Sent Events Bonus Round Profit

    Ken Terence Ruby Karaoke Friday Hug ps://www.flickr.com/photos/turbojoe/556776940
  5. WebSockets Handshake Request GET /chat HTTP/1.1 Host: server.example.com Upgrade: websocket

    Connection: Upgrade Sec-WebSocket-Key: x3JJHMbDL1EzLkh9... Sec-WebSocket-Protocol: chat, superchat Sec-WebSocket-Version: 13 Origin: http://example.com
  6. WebSockets Handshake Response HTTP/1.1 101 Switching Protocols Upgrade: websocket Connection:

    Upgrade Sec-WebSocket-Accept:HSmrc0sMlYUk... Sec-WebSocket-Protocol: chat
  7. Register Handlers EM.run { ws = Faye::WebSocket::Client.new('ws://www.example.com/') ws.on :open do

    |event| p [:open] ws.send('Hello, world!') end ws.on :message do |event| p [:message, event.data] end ws.on :close do |event| p [:close, event.code, event.reason] ws = nil end }
  8. def initialize(app) @app = app @clients = [] uri =

    URI.parse(ENV["REDISCLOUD_URL"]) @redis = Redis.new(host: uri.host, port: uri.port, password: uri.password) Thread.new do redis_sub = Redis.new(host: uri.host, port: uri.port, password: uri.password) redis_sub.subscribe(CHANNEL) do |on| on.message do |channel, msg| @clients.each {|ws| ws.send(msg) } end end end end
  9. def initialize(app) @app = app @clients = [] uri =

    URI.parse(ENV["REDISCLOUD_URL"]) @redis = Redis.new(host: uri.host, port: uri.port, password: uri.password) Thread.new do redis_sub = Redis.new(host: uri.host, port: uri.port, password: uri.password) redis_sub.subscribe(CHANNEL) do |on| on.message do |channel, msg| @clients.each {|ws| ws.send(msg) } end end end end
  10. if Faye::WebSocket.websocket?(env) ws = Faye::WebSocket.new(env, nil, {ping: KEEPALIVE_TIME }) ws.on

    :open do |event| p [:open, ws.object_id] @clients << ws end ws.on :message do |event| p [:message, event.data] @redis.publish(CHANNEL, sanitize(event.data)) end ws.on :close do |event| p [:close, ws.object_id, event.code, event.reason] @clients.delete(ws) ws = nil end # Return async Rack response ws.rack_response
  11. if Faye::WebSocket.websocket?(env) ws = Faye::WebSocket.new(env, nil, {ping: KEEPALIVE_TIME }) ws.on

    :open do |event| p [:open, ws.object_id] @clients << ws end ws.on :message do |event| p [:message, event.data] @redis.publish(CHANNEL, sanitize(event.data)) end ws.on :close do |event| p [:close, ws.object_id, event.code, event.reason] @clients.delete(ws) ws = nil end # Return async Rack response ws.rack_response
  12. if Faye::WebSocket.websocket?(env) ws = Faye::WebSocket.new(env, nil, {ping: KEEPALIVE_TIME }) ws.on

    :open do |event| p [:open, ws.object_id] @clients << ws end ws.on :message do |event| p [:message, event.data] @redis.publish(CHANNEL, sanitize(event.data)) end ws.on :close do |event| p [:close, ws.object_id, event.code, event.reason] @clients.delete(ws) ws = nil end # Return async Rack response ws.rack_response
  13. if Faye::WebSocket.websocket?(env) ws = Faye::WebSocket.new(env, nil, {ping: KEEPALIVE_TIME }) ws.on

    :open do |event| p [:open, ws.object_id] @clients << ws end ws.on :message do |event| p [:message, event.data] @redis.publish(CHANNEL, sanitize(event.data)) end ws.on :close do |event| p [:close, ws.object_id, event.code, event.reason] @clients.delete(ws) ws = nil end # Return async Rack response ws.rack_response
  14. if Faye::WebSocket.websocket?(env) ws = Faye::WebSocket.new(env, nil, {ping: KEEPALIVE_TIME }) ws.on

    :open do |event| p [:open, ws.object_id] @clients << ws end ws.on :message do |event| p [:message, event.data] @redis.publish(CHANNEL, sanitize(event.data)) end ws.on :close do |event| p [:close, ws.object_id, event.code, event.reason] @clients.delete(ws) ws = nil end # Return async Rack response ws.rack_response
  15. if Faye::WebSocket.websocket?(env) ws = Faye::WebSocket.new(env, nil, {ping: KEEPALIVE_TIME }) ws.on

    :open do |event| p [:open, ws.object_id] @clients << ws end ws.on :message do |event| p [:message, event.data] @redis.publish(CHANNEL, sanitize(event.data)) end ws.on :close do |event| p [:close, ws.object_id, event.code, event.reason] @clients.delete(ws) ws = nil end # Return async Rack response ws.rack_response
  16. <input type="text" id="msg-input" placeholder="msg here"></input> <br/> <button id="send-msg">Send</button> <script> var

    ws = new WebSocket('ws://localhost:4567/'); ws.onmessage = function(e) { alert(e.data); }; $('#send-msg').on('click', function(e) { e.preventDefault(); ws.send($('#msg-input').val()); $('#msg-input').val(''); }); </script>
  17. <input type="text" id="msg-input" placeholder="msg here"></input> <br/> <button id="send-msg">Send</button> <script> var

    ws = new WebSocket('ws://localhost:4567/'); ws.onmessage = function(e) { alert(e.data); }; $('#send-msg').on('click', function(e) { e.preventDefault(); ws.send($('#msg-input').val()); $('#msg-input').val(''); }); </script>
  18. <input type="text" id="msg-input" placeholder="msg here"></input> <br/> <button id="send-msg">Send</button> <script> var

    ws = new WebSocket('ws://localhost:4567/'); ws.onmessage = function(e) { alert(e.data); }; $('#send-msg').on('click', function(e) { e.preventDefault(); ws.send($('#msg-input').val()); $('#msg-input').val(''); }); </script>
  19. <input type="text" id="msg-input" placeholder="msg here"></input> <br/> <button id="send-msg">Send</button> <script> var

    ws = new WebSocket('ws://localhost:4567/'); ws.onmessage = function(e) { alert(e.data); }; $('#send-msg').on('click', function(e) { e.preventDefault(); ws.send($('#msg-input').val()); $('#msg-input').val(''); }); </script>
  20. require 'angelo' class Casc < Angelo::Base websocket '/' do |ws|

    websockets << ws ws.on_message do |msg| websockets.each {|s| s << msg} end end end Casc.run Websocket Route Builder
  21. require 'angelo' class Casc < Angelo::Base websocket '/' do |ws|

    websockets << ws ws.on_message do |msg| websockets.each {|s| s << msg} end end end Casc.run Websocket Route Builder
  22. require 'angelo' class Casc < Angelo::Base websocket '/' do |ws|

    websockets << ws ws.on_message do |msg| websockets.each {|s| s << msg} end end end Casc.run Websocket Route Builder
  23. HEART = '❤' task :hearts do every 1 do websockets[:hearts].each

    {|ws| ws.write HEART} end end websocket '/hearts' do |ws| async :hearts websockets[:hearts] << ws end async - go off and do this thing
  24. HEART = '❤' task :hearts do every 1 do websockets[:hearts].each

    {|ws| ws.write HEART} end end websocket '/hearts' do |ws| async :hearts websockets[:hearts] << ws end async - go off and do this thing
  25. HEART = '❤' task :hearts do every 1 do websockets[:hearts].each

    {|ws| ws.write HEART} end end websocket '/hearts' do |ws| async :hearts websockets[:hearts] << ws end async - go off and do this thing
  26. HEART = '❤' task :hearts do every 1 do websockets[:hearts].each

    {|ws| ws.write HEART} end end websocket '/hearts' do |ws| async :hearts websockets[:hearts] << ws end async - go off and do this thing
  27. HEART = '❤' task :hearts do every 1 do websockets[:hearts].each

    {|ws| ws.write HEART} end end websocket '/hearts' do |ws| async :hearts websockets[:hearts] << ws end async - go off and do this thing
  28. websocket '/' do |ws| websockets << ws async :subscribe, Redis::CHANNEL_KEY

    unless @@subscribed ws.on_message do |msg| Redis.with do |r| r.publish Redis::CHANNEL_KEY, sanitize(msg) end end end
  29. websocket '/' do |ws| websockets << ws async :subscribe, Redis::CHANNEL_KEY

    unless @@subscribed ws.on_message do |msg| Redis.with do |r| r.publish Redis::CHANNEL_KEY, sanitize(msg) end end end
  30. websocket '/' do |ws| websockets << ws async :subscribe, Redis::CHANNEL_KEY

    unless @@subscribed ws.on_message do |msg| Redis.with do |r| r.publish Redis::CHANNEL_KEY, sanitize(msg) end end end
  31. websocket '/' do |ws| websockets << ws async :subscribe, Redis::CHANNEL_KEY

    unless @@subscribed ws.on_message do |msg| Redis.with do |r| r.publish Redis::CHANNEL_KEY, sanitize(msg) end end end
  32. task :subscribe do |channel| @@subscribed = true catch :empty do

    r = Redis.new_redis r.subscribe channel do |on| on.message do |c, msg| websockets.each {|ws| ws.write msg} throw :empty if websockets.length == 0 end end end @@subscribed = false end
  33. task :subscribe do |channel| @@subscribed = true catch :empty do

    r = Redis.new_redis r.subscribe channel do |on| on.message do |c, msg| websockets.each {|ws| ws.write msg} throw :empty if websockets.length == 0 end end end @@subscribed = false end
  34. class Casc < Angelo::Base post '/' do sses.each {|sse| sse.write

    sse_message(params[:msg])} end eventsource '/' do |es| sses << es end end Casc.run Eventsource route builder
  35. class Casc < Angelo::Base post '/' do sses.each {|sse| sse.write

    sse_message(params[:msg])} end eventsource '/' do |es| sses << es end end Casc.run Eventsource route builder
  36. Eventsource route helper class Casc < Angelo::Base post '/' do

    sses.each {|sse| sse.write sse_message(params[:msg])} end get '/' do eventsource do |es| sses << es end end end Casc.run
  37. class Casc < Angelo::Base post '/' do sses.each {|sse| sse.write

    sse_message(params[:msg])} end eventsource '/' do |es| sses << es end end Casc.run Eventsource route builder
  38. Event creation helpers SSE_DATA_TEMPLATE = "data: %s\n\n" SSE_EVENT_TEMPLATE = "event:

    %s\ndata: %s\n\n" def sse_event event_name, data data = data.to_json if Hash === data SSE_EVENT_TEMPLATE % [event_name.to_s, data] end def sse_message data data = data.to_json if Hash === data SSE_DATA_TEMPLATE % data end
  39. post '/' do content_type :json Redis.with do |r| r.publish Redis::CHANNEL_KEY,

    sanitize(params[:msg].to_json) end PUBLISHED end eventsource '/sse/' do |es| sses << es async :subscribe, Redis::CHANNEL_KEY unless @@subscribed end
  40. post '/' do content_type :json Redis.with do |r| r.publish Redis::CHANNEL_KEY,

    sanitize(params[:msg].to_json) end PUBLISHED end eventsource '/sse/' do |es| sses << es async :subscribe, Redis::CHANNEL_KEY unless @@subscribed end
  41. task :subscribe do |channel| @@subscribed = true catch :empty do

    r = Redis.new_redis r.subscribe channel do |on| on.message do |c, msg| sses.each {|es| es.write sse_message(msg)} throw :empty if sses.length == 0 end end end @@subscribed = false end
  42. <input type="text" id="msg-input" placeholder="msg here"></input> <br/> <button id="send-msg">Send</button> <script> var

    ws = new WebSocket('ws://localhost:4567/'); ws.onmessage = function(e) { alert(e.data); }; $('#send-msg').on('click', function(e) { e.preventDefault(); ws.send($('#msg-input').val()); $('#msg-input').val(''); }); </script>
  43. <input type="text" id="msg-input" placeholder="msg here"></input> <br/> <button id="send-msg">Send</button> <script> var

    es = new EventSource('/'); es.onmessage = function(e) { alert(e.data); }; $('#send-msg').on('click', function(e) { e.preventDefault(); $.post('/', {msg: $('#msg-input').val()}); $('#msg-input').val(''); }); </script>
  44. before do # runs before every route handler end get

    '/' do content_type :json raise RequestError.new foo: 'msg' end post '/' do halt 404 if params[:nope] end after do # runs after every route handler # unless RequestError is raised end
  45. Angelo::Tilt::ERB class Casc < Angelo::Base include Angelo::Tilt::ERB get '/' do

    erb :index end end __END__ /views layout.html.erb index.html.erb
  46. Angelo::Mustermann class Casc < Angelo::Base include Angelo::Tilt::ERB include Angelo::Mustermann get

    '/:foo/bar' do @foo = params[:foo] erb :index end get '/(avatar|user)/(?<device_id>.*).jpg', type: :regexp do @img_name = params[:device_id] erb :avatar end end