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

Unraveling the Cable: How Action Cable Works

Unraveling the Cable: How Action Cable Works

Rails ships with some handy features, and most of that functionality is there because we have common problems; and those problems need a default solution. You need quick two-way communication between your client and server. Action Cable can solve that quickly, and get you up and productive with out setting up expensive external add-ons and complex integrations.

This magic is wonderful until you need to untangle how websockets, connections, channels, consumers, and clients all fit together. Let's look under the hood and see how everything is woven together.

Christopher Sexton

April 30, 2019
Tweet

More Decks by Christopher Sexton

Other Decks in Programming

Transcript

  1. @crsexton Unraveling the Cable How Action Cable Works Enough of

    that Let's get to the technical bits already
  2. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  3. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE *YAWN* ~~ Anxiety ~~
  4. @crsexton Action Cable Lifecycle 1. Client connects to cable 2.

    Server confirms the connection 3. Client subscribes to channel 4. Client sends messages to server 5. Server sends messages to client
  5. @crsexton Action Cable Lifecycle 1. Client connects to cable 2.

    Server confirms the connection 3. Client subscribes to channel 4. Client sends messages to server 5. Server sends messages to client import { createConsumer } from "@rails/actioncable" export default createConsumer() app/javascript/channels/consumer.js
  6. @crsexton Action Cable Lifecycle 1. Client connects to cable 2.

    Server confirms the connection 3. Client subscribes to channel 4. Client sends messages to server 5. Server sends messages to client module ApplicationCable class Connection < ActionCable::Connection::Base identified_by :current_user def connect self.current_user = find_user end protected def find_user User.find_by(id: cookies.signed[:user_id]) end end end app/javascript/channels/room_channel.js
  7. @crsexton Action Cable Lifecycle 1. Client connects to cable 2.

    Server confirms the connection 3. Client subscribes to channel 4. Client sends messages to server 5. Server sends messages to client consumer.subscriptions.create("RoomChannel", { received(data) { // Handle Data }, speak: function(message) { return this.perform('speak', {message: message}); } }); app/javascript/channels/room_channel.js
  8. @crsexton Action Cable Lifecycle 1. Client connects to cable 2.

    Server confirms the connection 3. Client subscribes to channel 4. Client sends messages to server 5. Server sends messages to client class RoomChannel < ApplicationCable::Channel def speak(data) Message.create! content: data["message"] end end app/channels/room_channel.rb consumer.subscriptions.create("RoomChannel", { speak: function(message) { return this.perform('speak', {message: message}); } }); app/javascript/channels/room_channel.js
  9. @crsexton Action Cable Lifecycle 1. Client connects to cable 2.

    Server confirms the connection 3. Client subscribes to channel 4. Client sends messages to server 5. Server sends messages to client ActionCable.server.broadcast "room_channel", message: "OHAI World" app/models/message.rb
  10. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  11. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  12. @crsexton Client connects to cable GET / HTTP/1.0 Host: localhost:3000

    HTTP/1.0 200 OK Content-Type: text/html Cache-Control: max-age=0 Set-Cookie: chocolate=chip <!DOCTYPE html> <body> ... CLIENT SERVER
  13. @crsexton Client connects to cable require 'socket' body = <<~EOF

    GET / HTTP/1.0\r Host: localhost:3000\r \r EOF Socket.tcp("localhost", 3000) {|sock| sock.print body sock.close_write puts sock.read }
  14. @crsexton Client connects to cable require 'socket' body = <<~EOF

    GET / HTTP/1.0\r Host: localhost:3000\r \r EOF Socket.tcp("localhost", 3000) {|sock| sock.print body sock.close_write puts sock.read } RAILS LOGS Completed 200 OK in 15ms (Views: 13.6ms | ActiveRecord: 0.2ms)
  15. @crsexton require 'socket' body = <<~EOF GET / HTTP/1.0\r Host:

    localhost:3000\r \r EOF Socket.tcp("localhost", 3000) {|sock| sock.print body sock.close_write puts sock.read } Client connects to cable
  16. @crsexton require 'socket' body = <<~EOF GET / HTTP/1.0\r Host:

    localhost:3000\r \r EOF Socket.tcp("localhost", 3000) {|sock| sock.print body sock.close_write puts sock.read } Client connects to cable RESPONSE HTTP/1.0 200 OK X-Frame-Options: SAMEORIGIN X-XSS-Protection: 1; mode=block X-Content-Type-Options: nosniff X-Download-Options: noopen X-Permitted-Cross-Domain-Policies: none Referrer-Policy: strict-origin-when-cross-origin Content-Type: text/html; charset=utf-8 ETag: W/"9f4da7fe69623bb6fe6c3efe5c907c06" Cache-Control: max-age=0, private, must-revalidate Set-Cookie: _unravel_session=lq1ryiEWvEoFCSLXrzVlLdkAzpAhQPWJMS X-Request-Id: 38dde761-2dda-4f57-93cf-2a4b0f871912 X-Runtime: 0.016541 <!DOCTYPE html> <html> <head> <title>Unravel</title> <meta name="csrf-param" content="authenticity_token" />
  17. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  18. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  19. @crsexton Client connects to cable body = <<~EOF.gsub("\n", "\r\n") GET

    ws://localhost:3000/cable HTTP/1.1\r Host: localhost:3000\r Connection: Upgrade\r Upgrade: websocket\r Origin: http://localhost:3000\r \r EOF Socket.tcp("localhost", 3000) {|sock| sock.print body sock.close_write puts sock.read }
  20. @crsexton Client connects to cable body = <<~EOF.gsub("\n", "\r\n") GET

    ws://localhost:3000/cable HTTP/1.1\r Host: localhost:3000\r Connection: Upgrade\r Upgrade: websocket\r Origin: http://localhost:3000\r \r EOF Socket.tcp("localhost", 3000) {|sock| sock.print body sock.close_write puts sock.read } LOGS Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
  21. @crsexton body = <<~EOF.gsub("\n", "\r\n") GET ws://localhost:3000/cable HTTP/1.1\r Host: localhost:3000\r

    Connection: Upgrade\r Upgrade: websocket\r Origin: http://localhost:3000\r \r EOF Socket.tcp("localhost", 3000) {|sock| sock.print body sock.close_write puts sock.read } Client connects to cable RESPONSE HTTP/1.1 101 Web Socket Protocol Handshake Upgrade: WebSocket Connection: Upgrade WebSocket-Origin: http://localhost:3000 WebSocket-Location: ws://localhost:3000 puts sock.read
  22. @crsexton body = <<~EOF.gsub("\n", "\r\n") GET ws://localhost:3000/cable HTTP/1.1\r Host: localhost:3000\r

    Connection: Upgrade\r Upgrade: websocket\r Origin: http://localhost:3000\r \r EOF Socket.tcp("localhost", 3000) {|sock| sock.print body sock.close_write puts sock.read } Client connects to cable RESPONSE HTTP/1.1 101 Web Socket Protocol Handshake Upgrade: WebSocket Connection: Upgrade WebSocket-Origin: http://localhost:3000 WebSocket-Location: ws://localhost:3000 puts sock.read
  23. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  24. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  25. @crsexton require 'rack' app = Proc.new do |env| [ '200',

    {'Content-Type' => 'text/html'}, ['Response Body'] ] end Rack::Handler::WEBrick.run app
  26. @crsexton require 'rack' app = Proc.new do |env| [ '200',

    {'Content-Type' => 'text/html'}, ['Response Body'] ] end Rack::Handler::WEBrick.run app
  27. @crsexton require 'rack' app = Proc.new do |env| [ '200',

    {'Content-Type' => 'text/html'}, ['Response Body'] ] end Rack::Handler::WEBrick.run app
  28. @crsexton require 'rack' app = Proc.new do |env| [ '200',

    {'Content-Type' => 'text/html'}, ['Response Body'] ] end Rack::Handler::WEBrick.run app
  29. @crsexton Server confirms the connection module ApplicationCable class Connection <

    ActionCable::Connection::Base identified_by :current_user def connect self.current_user = find_verified_user end private def find_verified_user User.find_by(id: cookies.encrypted[:user_id]) end end end app/channels/application_cable/connection.rb
  30. @crsexton Server confirms the connection module ApplicationCable class Connection <

    ActionCable::Connection::Base identified_by :current_user def connect self.current_user = find_verified_user end private def find_verified_user User.find_by(id: cookies.encrypted[:user_id]) end end end app/channels/application_cable/connection.rb
  31. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  32. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  33. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  34. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  35. @crsexton Redis Pub/Sub % Server Client % redis-cli 127.0.0.1:6379> SUBSCRIBE

    chan1 Reading messages... 1) "subscribe" 2) "chan1" 3) (integer) 1 ⏎
  36. @crsexton Redis Pub/Sub % redis-cli Server Client % redis-cli 127.0.0.1:6379>

    SUBSCRIBE chan1 Reading messages... 1) "subscribe" 2) "chan1" 3) (integer) 1
  37. @crsexton Redis Pub/Sub % redis-cli 127.0.0.1:6379> Server Client % redis-cli

    127.0.0.1:6379> SUBSCRIBE chan1 Reading messages... 1) "subscribe" 2) "chan1" 3) (integer) 1 ⏎
  38. @crsexton Redis Pub/Sub % redis-cli 127.0.0.1:6379> PUBLISH chan1 hi Server

    Client % redis-cli 127.0.0.1:6379> SUBSCRIBE chan1 Reading messages... 1) "subscribe" 2) "chan1" 3) (integer) 1 ⏎
  39. @crsexton Redis Pub/Sub % redis-cli 127.0.0.1:6379> PUBLISH chan1 hi Server

    Client ⏎ % redis-cli 127.0.0.1:6379> SUBSCRIBE chan1 Reading messages... 1) "subscribe" 2) "chan1" 3) (integer) 1 1) "message" 2) "chan1" 3) "hi"
  40. @crsexton Redis Pub/Sub Server Client % redis-cli 127.0.0.1:6379> SUBSCRIBE chan1

    Reading messages... 1) "subscribe" 2) "chan1" 3) (integer) 1 1) "message" 2) "chan1" 3) "hi" % redis-cli 127.0.0.1:6379> PUBLISH chan1 hi (integer) 1 127.0.0.1:6379> PUBLISH chan1 hi
  41. @crsexton Redis Pub/Sub % redis-cli 127.0.0.1:6379> SUBSCRIBE chan1 Reading messages...

    1) "subscribe" 2) "chan1" 3) (integer) 1 1) "message" 2) "chan1" 3) "hi" 1) "message" 2) "chan1" 3) "hi" % redis-cli 127.0.0.1:6379> PUBLISH chan1 hi (integer) 1 127.0.0.1:6379> PUBLISH chan1 hi (integer) 1 Server Client ⏎
  42. @crsexton Redis Pub/Sub require 'redis' $redis = Redis.new $redis.subscribe('chan1') do

    |on| on.message do |channel, msg| puts "1) message" puts "2) #{channel}" puts "3) #{msg}" end end require 'redis' $redis = Redis.new $redis.publish "chan1", ARGV[0]
  43. @crsexton Redis Pub/Sub Server Client % ruby sub.rb 1) message

    2) chan1 3) hi % ruby pub.rb hi
 % ruby pub.rb there
  44. @crsexton Redis Pub/Sub Server Client % ruby sub.rb 1) message

    2) chan1 3) hi 1) message 2) chan1 3) there % ruby pub.rb hi
 % ruby pub.rb there ⏎
  45. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  46. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  47. @crsexton How WebSockets Work 0 1 2 3 0 1

    2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ RFC 6455
  48. @crsexton (Like this helps) 0 1 2 3 0 1

    2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 +-+-+-+-+-------+-+-------------+-------------------------------+ |F|R|R|R| opcode|M| Payload len | Extended payload length | |I|S|S|S| (4) |A| (7) | (16/64) | |N|V|V|V| |S| | (if payload len==126/127) | | |1|2|3| |K| | | +-+-+-+-+-------+-+-------------+ - - - - - - - - - - - - - - - + | Extended payload length continued, if payload len == 127 | + - - - - - - - - - - - - - - - +-------------------------------+ | |Masking-key, if MASK set to 1 | +-------------------------------+-------------------------------+ | Masking-key (continued) | Payload Data | +-------------------------------- - - - - - - - - - - - - - - - + : Payload Data continued ... : + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + | Payload Data continued ... | +---------------------------------------------------------------+ RFC 6455
  49. @crsexton 2-10 BYTES How WebSockets Work 2 BYTES 4 BYTES

    N BYTES OPCODE LENGTH EXT LENGTH MASK DATA Client Only
  50. @crsexton OPCODE LENGTH EXT LENGTH MASK DATA Client Only How

    WebSockets Work 2 BYTES 4 BYTES N BYTES NOT TO SCALE
  51. @crsexton Client Cable Server WebSocket Web Server Redis Workers Other

    Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  52. @crsexton Sending messages to client 1. Broadcast to Redis Pub/Sub

    2. Publish to Cable Server 3. Send message over socket 4. Receive message in consumer
  53. @crsexton Sending messages to client ActionCable .server .broadcast "my_channel", message:

    "hi", one: 1 JSON { "identifier" : "{\"channel\":\"MyChannel\"}", "message" : { "message" : "hi", "one" : 1 } }
  54. @crsexton Sending messages to client import consumer from "./consumer" window.room

    = consumer.subscriptions.create("RoomChannel", { received(data) { var div = document.getElementById('messages'); div.innerHTML += data['message']; }, }); JSON { "message" : { "message" : "hi", "one" : 1 }, "identifier" : "{\"channel\":\"RoomChannel\"}" }
  55. @crsexton Client Cable Server WebSocket Web Server Redis Other Traffic

    Workers Other Traffic GET 101 Switch Protocol PUBLISH/SUBSCRIBE
  56. @crsexton Recap 1. Client connects to cable 2. Server confirms

    the connection 3. Client subscribes to channel 4. Client sends messages to server 5. Server sends messages to client Cable Server Web Server Redis Other Traffic Client
  57. @crsexton Recap 1. Client connects to cable 2. Server confirms

    the connection 3. Client subscribes to channel 4. Client sends messages to server 5. Server sends messages to client Cable Server Web Server Redis Other Traffic Client
  58. @crsexton Recap 1. Client connects to cable 2. Server confirms

    the connection 3. Client subscribes to channel 4. Client sends messages to server 5. Server sends messages to client Cable Server Web Server Redis Other Traffic Client
  59. @crsexton Recap 1. Client connects to cable 2. Server confirms

    the connection 3. Client subscribes to channel 4. Client sends messages to server 5. Server sends messages to client Cable Server Web Server Redis Other Traffic Client
  60. @crsexton Recap 1. Client connects to cable 2. Server confirms

    the connection 3. Client subscribes to channel 4. Client sends messages to server 5. Server sends messages to client Cable Server Web Server Redis Other Traffic Client
  61. @crsexton Recap 1. Client connects to cable 2. Server confirms

    the connection 3. Client subscribes to channel 4. Client sends messages to server 5. Server sends messages to client Cable Server Web Server Redis Other Traffic Client