WebSocket
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
- Descripción general del ciclo de vida
- Paso 1: handshake de WebSocket
- Paso 2: STOMP CONNECT
- Frame CONNECT
- Respuesta esperada: frame CONNECTED
- Construcción de frames STOMP
- Paso 3: suscribirse a un tópico
- Frame SUBSCRIBE
- Paso 4: bucle de mensajes y heartbeats
- Mecanismo de heartbeat
- Análisis de frames entrantes
- Bucle de mensajes
- Paso 5: desconexión adecuada
- Garantías de entrega
- Cuando el servidor cierra la conexión
- Estrategia de reconexión
- Bucle completo de reconexión
- Tiempos de espera de la conexión
- Problemas comunes
Esta página se tradujo automáticamente. Si encuentras errores o tienes sugerencias, escríbenos.
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):
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| Encabezado | Valor | Descripción |
|---|---|---|
accept-version | 1.2 | Versión del protocolo STOMP |
heart-beat | 10000,10000 | Intervalo de envío/recepción de heartbeat del cliente en milisegundos |
Respuesta esperada: frame CONNECTED
CONNECTED
version:1.2
heart-beat:10000,10000
\x00El 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()function buildStompFrame(command, headers = {}, body = "") {
let frame = command + "\n";
for (const [key, value] of Object.entries(headers)) {
frame += `${key}:${value}\n`;
}
frame += "\n";
frame += body;
frame += "\x00";
return frame;
}
// Send CONNECT
ws.send(
buildStompFrame("CONNECT", {
"accept-version": "1.2",
"heart-beat": "10000,10000",
})
);func buildStompFrame(command string, headers map[string]string, body string) string {
var b strings.Builder
b.WriteString(command + "\n")
for k, v := range headers {
b.WriteString(fmt.Sprintf("%s:%s\n", k, v))
}
b.WriteString("\n")
b.WriteString(body)
b.WriteByte(0x00)
return b.String()
}
// Send CONNECT
connectFrame := buildStompFrame("CONNECT", map[string]string{
"accept-version": "1.2",
"heart-beat": "10000,10000",
}, "")
conn.WriteMessage(websocket.TextMessage, []byte(connectFrame))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| Encabezado | Valor | Descripción |
|---|---|---|
id | sub-0 | Identificador de suscripción definido por el cliente |
destination | /topic/ | El tópico al que suscribirse |
subscribe_frame = build_stomp_frame("SUBSCRIBE", {
"id": "sub-0",
"destination": f"/topic/change/{org_uuid}"
})
ws.send(subscribe_frame)Reemplaza {orgUuid} con el UUID de tu organización. Recupéralo a través del endpoint REST POST /api/org/getOrgV2.
Paso 4: bucle de mensajes y heartbeats
Una vez suscrito, tu aplicación entra en un bucle de mensajes donde:
- Recibe frames MESSAGE que contienen datos de eventos
- Envía heartbeats para mantener viva la conexión
- 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.
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
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
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:
# 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
| Cierre | Causa | Qué hacer |
|---|---|---|
STOMP ERROR y luego código de cierre 1008 | La 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 menos | Revisa 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 1002 | Una suscripción al tópico de otra organización, o un frame que el servidor no acepta | Corrige la suscripción; reconectarte con los mismos frames falla de la misma forma |
Cierre sin frame ERROR | Heartbeats perdidos, ningún CONNECT en 60 segundos, un cliente lento, o un reinicio de la red o del servidor | Reconéctate con backoff |
Estrategia de reconexión
Las interrupciones de red son inevitables. Implementa una reconexión automática con backoff:
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
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:
passTiempos de espera de la conexión
| Timeout | Valor | Descripción |
|---|---|---|
| Handshake | 10 segundos | Tiempo máximo para completar la actualización de WebSocket |
| Heartbeat | 10 segundos | Intervalo entre pings de keep-alive |
| Conexión muerta | ~30 segundos | Tiempo aproximado antes de que un heartbeat faltante dispare la desconexión |
| Primera trama | 60 segundos | Envía la trama STOMP CONNECT dentro de este tiempo tras abrir el socket, o se cierra |
| Cliente lento | 10 segundos o 512 KiB | Se desconecta un cliente al que el servidor no puede enviar durante 10 segundos o que se retrasa 512 KiB |
Problemas comunes
| Síntoma | Causa probable | Solución |
|---|---|---|
| La conexión se cae cada ~30s | No se están enviando heartbeats | Implementa el thread emisor de heartbeats |
El frame CONNECTED nunca se recibe | Frame STOMP CONNECT mal formado | Verifica que el formato del frame incluya el terminador nulo \x00 |
| No se reciben mensajes | Tópico incorrecto o falta el UUID de la organización | Verifica 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ón | La 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 |