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
ChatChannelczyNotificationsChannel, 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.