En 2015, Chris McCord demostró Phoenix manejando 2 millones de conexiones WebSocket simultáneas en un solo servidor. No era un truco de marketing: era la demostración de que la BEAM, con sus procesos ligeros y su scheduler de baja latencia, cambia completamente las reglas sobre cuántos usuarios concurrentes puede manejar un servidor web. Phoenix Channels es la abstracción que hace eso accesible.
Qué es un Channel
Un Channel es un módulo que gestiona la comunicación bidireccional entre el cliente (navegador, app móvil, otro servidor) y el servidor, a través de un topic. El topic es como un canal de radio: varios clientes pueden suscribirse al mismo topic y recibir los mismos mensajes.
defmodule MiAppWeb.ChatChannel do
use Phoenix.Channel
def join("sala:" <> sala_id, _payload, socket) do
if autorizado?(socket.assigns.usuario_id, sala_id) do
send(self(), :after_join)
{:ok, assign(socket, :sala_id, sala_id)}
else
{:error, %{razon: "no autorizado"}}
end
end
def handle_info(:after_join, socket) do
mensajes = Chat.ultimos_mensajes(socket.assigns.sala_id)
push(socket, "historial", %{mensajes: mensajes})
{:noreply, socket}
end
def handle_in("nuevo_mensaje", %{"texto" => texto}, socket) do
usuario_id = socket.assigns.usuario_id
{:ok, mensaje} = Chat.guardar_mensaje(socket.assigns.sala_id, usuario_id, texto)
broadcast!(socket, "mensaje_recibido", %{
id: mensaje.id,
texto: mensaje.texto,
usuario: mensaje.usuario.nombre
})
{:noreply, socket}
end
def handle_in("escribiendo", _payload, socket) do
broadcast_from!(socket, "usuario_escribiendo", %{usuario_id: socket.assigns.usuario_id})
{:noreply, socket}
end
end
El Socket y el Router
Los channels se conectan a través de un Socket, que gestiona la autenticación y el ciclo de vida de la conexión WebSocket:
defmodule MiAppWeb.UserSocket do
use Phoenix.Socket
channel "sala:*", MiAppWeb.ChatChannel
channel "notificaciones:*", MiAppWeb.NotificacionesChannel
def connect(%{"token" => token}, socket, _connect_info) do
case Phoenix.Token.verify(MiAppWeb.Endpoint, "user_token", token, max_age: 86_400) do
{:ok, usuario_id} -> {:ok, assign(socket, :usuario_id, usuario_id)}
{:error, _} -> :error
end
end
def id(socket), do: "users_socket:#{socket.assigns.usuario_id}"
end
El pattern "sala:*" indica que el ChatChannel maneja cualquier topic que empiece por "sala:". Esta convención de topics jerárquicos es la forma estándar de organizar los namespaces en Phoenix.
PubSub: mensajes entre nodos
Por debajo de los Channels está Phoenix.PubSub, un sistema de publicación/suscripción que funciona tanto en un único nodo como en un cluster distribuido. Cuando haces broadcast! en un Channel, el mensaje llega a todos los procesos suscritos a ese topic, incluso si están en servidores diferentes.
# Suscribirse desde cualquier parte del código
Phoenix.PubSub.subscribe(MiApp.PubSub, "pedidos:nuevos")
# Publicar desde cualquier parte (incluso fuera de un Channel)
Phoenix.PubSub.broadcast(MiApp.PubSub, "pedidos:nuevos", %{
pedido_id: 123,
estado: :recibido
})
# El proceso suscrito recibe el mensaje en handle_info
def handle_info(%{pedido_id: id, estado: estado}, state) do
IO.puts("Nuevo pedido #{id}: #{estado}")
{:noreply, state}
end
Presence: quién está conectado
Phoenix.Presence rastrea qué usuarios están conectados a qué topics, con sincronización automática entre nodos del cluster:
defmodule MiApp.Presence do
use Phoenix.Presence,
otp_app: :mi_app,
pubsub_server: MiApp.PubSub
end
# En el Channel
def handle_info(:after_join, socket) do
{:ok, _} = MiApp.Presence.track(socket, socket.assigns.usuario_id, %{
nombre: socket.assigns.nombre,
conectado_el: DateTime.utc_now()
})
push(socket, "presencia_inicial", MiApp.Presence.list(socket))
{:noreply, socket}
end
Channels vs LiveView: cuándo usar cada uno
Channels y LiveView usan la misma infraestructura WebSocket pero sirven para cosas distintas. LiveView gestiona interfaces HTML reactivas desde el servidor. Channels son para comunicación bidireccional de propósito general: juegos multijugador, APIs para apps móviles, integraciones entre servicios, o cualquier caso donde el cliente necesite enviar y recibir mensajes arbitrarios sin actualizar el DOM.
Puedes usar ambos en el mismo proyecto sin conflicto: el endpoint de Phoenix gestiona tanto las conexiones LiveView como las conexiones de Channel sobre el mismo puerto.
Consumo de recursos
Cada conexión Channel es un proceso Elixir ligero, unos pocos kilobytes. En comparación, una conexión gestionada con Node.js o con threads del sistema operativo tiene un overhead considerablemente mayor. Eso es lo que permite el número que demostró McCord en 2015 y que sigue siendo válido: la BEAM no comparte el modelo de "un thread por conexión" que limita otros servidores web.
Para ver cómo Phoenix LiveView usa esta misma infraestructura para interfaces reactivas, el artículo sobre LiveView de esta serie es el complemento directo. Y el artículo sobre channels en Go muestra un mecanismo de comunicación entre procesos con un nombre parecido pero una función completamente diferente.
Imagen: Pexels / panumas nikhomkhai
