Skip to content
← Wszystkie wpisy
4 min czytania Dawid Skłodowski

WebSockety w Rails 5 z ActionCable

Rails 5 wnosi ActionCable — WebSockety zintegrowane z frameworkiem. Deep-dive o kanałach, subskrypcjach i broadcastowaniu, jak działa pub/sub oparty na Redisie i o realiach skalowania trzymania tysięcy otwartych połączeń.

Web zbudowano na modelu żądanie-odpowiedź: przeglądarka pyta, serwer odpowiada, połączenie się zamyka. To kiepskie dopasowanie do czegokolwiek na żywo — czatu, powiadomienia, tickera cen, edytora współpracy — gdzie serwer musi pchać dane do przeglądarki bez bycia o to pytanym. Latami udawaliśmy to pollingiem (pytaj co kilka sekund) albo long-pollingiem (trzymaj żądanie otwarte), oba niezgrabne i marnotrawne. Rails 5, obecnie w becie, wnosi porządną odpowiedź do frameworka: ActionCable, WebSockety zintegrowane z resztą Rails. Oto jak działa i na co uważać.

WebSockety: trwała, dwukierunkowa rura

WebSocket to pojedyncze, długo żyjące połączenie TCP, które pozostaje otwarte i pozwala danym płynąć w obie strony w dowolnym momencie. Po początkowym handshake’u HTTP, który „podnosi” połączenie, serwer może wepchnąć wiadomość do przeglądarki w chwili, gdy coś się stanie, a przeglądarka może odesłać jedną z powrotem, wszystko po tym samym połączeniu bez narzutu nowych żądań HTTP. W porównaniu z pollingiem jest dramatycznie wydajniejszy (bez ciągłego ponownego odpytywania) i naprawdę czasu rzeczywistego (bez opóźnienia interwału pollingu). ActionCable to framework Rails do używania WebSocketów bez ręcznego klepania całej maszynerii.

Elementy: połączenia, kanały, subskrypcje

ActionCable ma małe słownictwo warte uporządkowania:

  • Connection jest ustanawiane raz na kartę przeglądarki (na WebSocket). Obsługuje uwierzytelnienie — kto jest po drugiej stronie — i istnieje przez życie gniazda.
  • Channel jest jak kontroler do pracy WebSocketowej: logiczna jednostka jak ChatChannel czy NotificationsChannel, z akcjami, które klienci mogą wołać, i strumieniami, do których może broadcastować.
  • Subscription to klient subskrybujący kanał, opcjonalnie zawężony — „ten użytkownik, subskrybujący pokój 42”.

Uwierzytelnienie dzieje się w Connection, używając tego samego ciasteczka sesji co reszta aplikacji:

# app/channels/application_cable/connection.rb
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]) || reject_unauthorized_connection
    end
  end
end

To część, którą ludzie pomijają i żałują: WebSocket to trwały, uwierzytelniony kanał do twojej aplikacji, i musisz zweryfikować tożsamość w czasie connect, bo wszystko, co połączenie robi potem, działa jako ten użytkownik.

Kanał i strumieniowanie

Kanał subskrybuje klienta do strumienia — nazwanego celu broadcastu. Gdy coś jest broadcastowane do tego strumienia, każdy subskrybent to otrzymuje:

# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_#{params[:room_id]}"
  end

  def speak(data)
    Message.create!(room_id: params[:room_id],
                    user: current_user, body: data["body"])
  end
end

Na serwerze broadcastujesz do tego strumienia skądkolwiek — zwykle callback modelu albo, lepiej, zadanie w tle — a każda subskrybowana przeglądarka dostaje payload wepchnięty do siebie:

ActionCable.server.broadcast("room_#{message.room_id}",
                             user: message.user.name, body: message.body)

Klient subskrybuje i reaguje na napływające dane odrobiną JavaScriptu:

App.cable.subscriptions.create({ channel: "ChatChannel", room_id: 42 }, {
  received(data) {
    appendMessage(data.user, data.body);   // serwer wepchnął to do nas
  },
  speak(body) {
    this.perform("speak", { body });        // zawołaj akcję kanału
  }
});

To działający czat czasu rzeczywistego w kilkudziesięciu liniach, z tymi samymi modelami, uwierzytelnieniem i konwencjami co reszta aplikacji Rails — co jest dokładnie integracją, którą ActionCable sprzedaje.

Jak broadcastowanie naprawdę działa: pub/sub Redisa

Warto zrozumieć hydraulikę, bo wyjaśnia historię skalowania. Gdy broadcastujesz do strumienia, ActionCable nie sięga jakoś sam do każdego połączenia. Publikuje wiadomość do backendu pub/sub — Redisa, na produkcji. Każdy proces serwera ActionCable subskrybuje Redisa, otrzymuje opublikowaną wiadomość i przekazuje ją w dół połączeń WebSocket, które trzyma dla subskrybentów tego strumienia.

Redis jako szyna wiadomości jest tym, co pozwala uruchomić wiele procesów ActionCable (albo serwerów): wiadomość broadcastowana na jednym procesie dociera do subskrybentów połączonych do innego, bo wszystkie dzielą Redisa. Bez tego broadcast dotarłby tylko do użytkowników, którzy akurat byli połączeni do tego samego procesu — bezużyteczne poza jedną maszyną.

Realia skalowania

Tu WebSockety wymagają innego nastawienia niż żądanie-odpowiedź i tu zespoły bywają zaskoczone:

  • Połączenia są trzymane otwarte i kosztują. Normalne żądanie webowe zajmuje workera na milisekundy. WebSocket zajmuje zasoby tak długo, jak użytkownik ma otwartą kartę — minuty lub godziny. Tysiące jednoczesnych użytkowników oznacza tysiące otwartych połączeń trzymanych jednocześnie, co jest fundamentalnie innym profilem obciążenia niż serwowanie stron.
  • Serwer cable często chce być osobny. Ponieważ długo żyjące połączenia wiążą zasoby serwera inaczej niż krótkie żądania, częste jest uruchamianie ActionCable jako własnego procesu/serwera, skalowanego niezależnie od zwykłych workerów webowych, by fala połączeń gniazd nie zagłodziła normalnego serwowania stron.
  • Potrzebujesz serwera zdarzeniowego. ActionCable trzyma wiele połączeń na proces, co wymaga modelu współbieżności do tego dopasowanego (Puma w trybie wątkowym/klastrowym); serwer proces-na-żądanie to złe narzędzie.
  • Broadcastuj z zadań w tle, nie wątków żądań. Wepchnięcie broadcastu inline w kontrolerze wiąże opóźnienie twojego żądania z fan-outem; wykonaj pracę i broadcastuj z zadania.

Werdykt

ActionCable to naprawdę cenny dodatek do Rails 5: wnosi funkcje czasu rzeczywistego, server-push do frameworka z tym samym uwierzytelnieniem, modelami i konwencjami, których już używasz, a „zbuduj czat” przechodzi z wielotygodniowej integracji w kilka kanałów. Projekt pub/sub oparty na Redisie jest solidny i pozwala mu skalować się między procesami. Ale WebSockety to nie darmowe HTTP — trzymanie tysięcy trwałych połączeń to inny profil obciążenia, który chce serwera zdarzeniowego, często osobnej warstwy cable i broadcastowania z zadań w tle. Używaj ActionCable tam, gdzie czas rzeczywisty naprawdę poprawia produkt — aktualizacje na żywo, powiadomienia, współpraca — i wejdź w to, rozumiejąc, że kosztem są połączenia trzymane otwarte, a nie żądania obsłużone. Złap to dobrze, a Rails potrafi teraz robić czas rzeczywisty równie komfortowo, co CRUD.