Ir al contenido

Ciclo de vida de la conexión WebSocket

Administra el ciclo de vida de una conexión WebSocket de Rhombus: realiza handshakes STOMP, envía heartbeats, maneja reconexiones y cierra correctamente.

En esta página

Esta guía cubre el ciclo de vida completo de una conexión WebSocket de Rhombus: establecer la conexión, realizar el handshake STOMP, mantener la conexión con heartbeats y manejar las desconexiones adecuadamente.

Descripción general del ciclo de vida

┌─────────────────┐
│  1. WebSocket    │
│     Handshake    │──── WSS + Auth Headers
└────────┬────────┘
         ▼
┌─────────────────┐
│  2. STOMP        │
│     CONNECT      │──── Version + Heartbeat negotiation
└────────┬────────┘
         ▼
┌─────────────────┐
│  3. SUBSCRIBE    │──── Topic subscription
└────────┬────────┘
         ▼
┌─────────────────┐
│  4. MESSAGE      │◄─── Real-time events
│     Loop         │───► Heartbeat ping/pong
└────────┬────────┘
         ▼
┌─────────────────┐
│  5. DISCONNECT   │──── Graceful shutdown
└─────────────────┘

Paso 1: handshake de WebSocket

Inicia una conexión WebSocket segura al endpoint de Rhombus con tu credencial. Este ejemplo usa una clave de API; los access tokens de OAuth y los tokens federados también funcionan (consulta Autenticación de WebSocket):

python
import websocket

ws = websocket.create_connection(
    "wss://ws.rhombussystems.com:8443/websocket?x-auth-scheme=api-token",
    header={"x-auth-apikey": API_TOKEN},
    timeout=10  # 10-second handshake timeout
)

Un handshake exitoso actualiza la conexión HTTP a WebSocket. Si la autenticación falla, el servidor responde antes de que se complete la actualización con un error HTTP y un cuerpo JSON cuyo errorCode indica el motivo: 401 cuando la credencial falta, es desconocida, expiró o fue revocada, y 403 cuando es válida pero no está permitida (por ejemplo, una credencial de partner sin x-auth-org). Consulta Autenticación de WebSocket.

Paso 2: STOMP CONNECT

Después de que se establezca la conexión WebSocket, envía un frame STOMP CONNECT para inicializar el protocolo de mensajería:

Frame CONNECT

CONNECT
accept-version:1.2
heart-beat:10000,10000

\x00
EncabezadoValorDescripción
accept-version1.2Versión del protocolo STOMP
heart-beat10000,10000Intervalo de envío/recepción de heartbeat del cliente en milisegundos

Respuesta esperada: frame CONNECTED

CONNECTED
version:1.2
heart-beat:10000,10000

\x00

El servidor devuelve la versión y los intervalos de heartbeat negociados.

Construcción de frames STOMP

Los frames STOMP siguen un formato simple basado en texto:

COMMAND\n
header1:value1\n
header2:value2\n
\n
body (optional)
\x00
  • Cada frame comienza con un nombre de comando
  • Los encabezados son pares clave-valor separados por dos puntos, uno por línea
  • Una línea vacía separa los encabezados del cuerpo
  • El frame termina con un byte nulo (\x00)
def build_stomp_frame(command, headers=None, body=""):
    """Construct a STOMP protocol frame."""
    frame = command + "\n"
    if headers:
        for key, value in headers.items():
            frame += f"{key}:{value}\n"
    frame += "\n"
    frame += body
    frame += "\x00"
    return frame

# Send CONNECT
connect_frame = build_stomp_frame("CONNECT", {
    "accept-version": "1.2",
    "heart-beat": "10000,10000"
})
ws.send(connect_frame)

# Read CONNECTED response
response = ws.recv()

Paso 3: suscribirse a un tópico

Después de establecer la conexión STOMP, suscríbete al tópico de cambios de la organización:

Frame SUBSCRIBE

SUBSCRIBE
id:sub-0
destination:/topic/change/{orgUuid}

\x00
EncabezadoValorDescripción
idsub-0Identificador de suscripción definido por el cliente
destination/topic/change/{orgUuid}El tópico al que suscribirse
python
subscribe_frame = build_stomp_frame("SUBSCRIBE", {
    "id": "sub-0",
    "destination": f"/topic/change/{org_uuid}"
})
ws.send(subscribe_frame)

Paso 4: bucle de mensajes y heartbeats

Una vez suscrito, tu aplicación entra en un bucle de mensajes donde:

  1. Recibe frames MESSAGE que contienen datos de eventos
  2. Envía heartbeats para mantener viva la conexión
  3. Recibe heartbeats del servidor

Mecanismo de heartbeat

Los heartbeats son caracteres de salto de línea individuales (\n) enviados al intervalo negociado (10 segundos). Tanto el cliente como el servidor envían heartbeats. Si alguno de los lados deja de recibir heartbeats, la conexión se considera muerta.

python
import threading
import time

def send_heartbeats(ws, stop_event):
    """Send STOMP heartbeats every 10 seconds."""
    while not stop_event.is_set():
        try:
            ws.send("\n")
        except Exception:
            break
        stop_event.wait(10)

# Start heartbeat sender in background
stop_heartbeat = threading.Event()
heartbeat_thread = threading.Thread(
    target=send_heartbeats,
    args=(ws, stop_heartbeat),
    daemon=True
)
heartbeat_thread.start()

Análisis de frames entrantes

python
def parse_stomp_frame(raw):
    """Parse a raw STOMP frame into command, headers, and body."""
    # Handle heartbeats (empty frames)
    raw = raw.lstrip("\n\r")
    if not raw or raw == "\x00":
        return None  # Heartbeat

    raw = raw.rstrip("\x00")
    parts = raw.split("\n\n", 1)

    lines = parts[0].split("\n")
    command = lines[0]

    headers = {}
    for line in lines[1:]:
        if ":" in line:
            key, value = line.split(":", 1)
            headers[key] = value

    body = parts[1] if len(parts) > 1 else ""
    return {"command": command, "headers": headers, "body": body}

Bucle de mensajes

python
try:
    while True:
        raw = ws.recv()
        frame = parse_stomp_frame(raw)

        if frame is None:
            continue  # Heartbeat, skip

        if frame["command"] == "MESSAGE":
            payload = json.loads(frame["body"])
            handle_event(payload)

except KeyboardInterrupt:
    print("Shutting down...")
finally:
    stop_heartbeat.set()
    heartbeat_thread.join()

Paso 5: desconexión adecuada

Antes de cerrar el WebSocket, envía un frame STOMP DISCONNECT:

python
# Send DISCONNECT frame
disconnect_frame = build_stomp_frame("DISCONNECT")
ws.send(disconnect_frame)

# Close WebSocket
ws.close()

Esto le indica al servidor que el cliente se está desconectando intencionalmente, permitiéndole liberar recursos de inmediato en lugar de esperar un timeout de heartbeat.

Garantías de entrega

El WebSocket de eventos entrega cada mensaje como máximo una vez: los mensajes no se confirman, no se guardan y no se reenvían, así que lo que se publica mientras estás desconectado se pierde, y no hay garantía de orden. Después de cada reconexión, lee lo que te perdiste con la API REST (consulta Crear una canalización de eventos) y ordena los eventos por sus marcas de tiempo. La referencia de eventos tiene los detalles.

Cuando el servidor cierra la conexión

CierreCausaQué hacer
STOMP ERROR y luego código de cierre 1008La credencial se eliminó o revocó, o su rol cambió. El servidor vuelve a verificar la credencial aproximadamente una vez por minuto, así que esto ocurre en 60 segundos o menosRevisa la credencial antes de reconectarte; después de una eliminación o revocación, todo handshake falla con 401
STOMP ERROR y luego código de cierre 1002Una suscripción al tópico de otra organización, o un frame que el servidor no aceptaCorrige la suscripción; reconectarte con los mismos frames falla de la misma forma
Cierre sin frame ERRORHeartbeats perdidos, ningún CONNECT en 60 segundos, un cliente lento, o un reinicio de la red o del servidorReconéctate con backoff

Estrategia de reconexión

Las interrupciones de red son inevitables. Implementa una reconexión automática con backoff:

python
import time

def connect_with_retry(api_token, org_uuid, max_retries=10):
    """Connect to Rhombus WebSocket with exponential backoff."""
    for attempt in range(max_retries):
        try:
            ws = websocket.create_connection(
                "wss://ws.rhombussystems.com:8443/websocket"
                "?x-auth-scheme=api-token",
                header={"x-auth-apikey": api_token},
                timeout=10
            )
            stomp_connect(ws)
            stomp_subscribe(ws, org_uuid)
            print("Connected successfully")
            return ws
        except Exception as e:
            delay = min(5 * (2 ** attempt), 60)
            print(f"Connection failed: {e}. Retrying in {delay}s...")
            time.sleep(delay)

    raise ConnectionError("Max retries exceeded")

Bucle completo de reconexión

python
while True:
    ws = connect_with_retry(API_TOKEN, ORG_UUID)

    try:
        run_message_loop(ws)
    except websocket.WebSocketConnectionClosedException:
        print("Connection lost. Reconnecting...")
    except KeyboardInterrupt:
        print("User interrupted. Exiting.")
        break
    finally:
        try:
            ws.close()
        except Exception:
            pass

Tiempos de espera de la conexión

TimeoutValorDescripción
Handshake10 segundosTiempo máximo para completar la actualización de WebSocket
Heartbeat10 segundosIntervalo entre pings de keep-alive
Conexión muerta~30 segundosTiempo aproximado antes de que un heartbeat faltante dispare la desconexión
Primera trama60 segundosEnvía la trama STOMP CONNECT dentro de este tiempo tras abrir el socket, o se cierra
Cliente lento10 segundos o 512 KiBSe desconecta un cliente al que el servidor no puede enviar durante 10 segundos o que se retrasa 512 KiB

Problemas comunes

SíntomaCausa probableSolución
La conexión se cae cada ~30sNo se están enviando heartbeatsImplementa el thread emisor de heartbeats
El frame CONNECTED nunca se recibeFrame STOMP CONNECT mal formadoVerifica que el formato del frame incluya el terminador nulo \x00
No se reciben mensajesTópico incorrecto o falta el UUID de la organizaciónVerifica que el UUID de la organización coincida con la organización de tu credencial (o con el cliente de x-auth-org)
Bucle de reconexiónLa credencial se eliminó, se revocó o expiró (401 en cada handshake), o la organización alcanzó el límite de solicitudes (429)Verifica la credencial con una llamada REST; ante un 429 espera Retry-After y aplica backoff