Tutoriales

MQTT sobre WebSockets en Python

Conecta paho-mqtt por WebSockets: transport="websockets", wss con TLS, path y cabeceras con ws_set_options(), proxies HTTP y soluciones a errores de handshake.

Actualizado · 8 min de lectura

MQTT sobre WebSockets en Python

Para usar MQTT sobre WebSockets en Python, crea el cliente paho con transport="websockets", conéctate al puerto WebSocket del broker y, si hace falta, define el path de la URL con ws_set_options(). Para wss cifrado, añade tls_set() y usa el puerto de WebSocket seguro. Por lo demás, la API MQTT es idéntica: los mismos callbacks, el mismo publish() y el mismo subscribe().

Esta guía muestra conexiones ws y wss, paths y cabeceras personalizados, la conexión a través de proxies HTTP y SOCKS, y cómo depurar los errores de handshake que lanza paho.

Cuándo tienen sentido los WebSockets desde Python

Los navegadores solo pueden hablar MQTT por WebSockets, pero Python puede usar TCP directo, que es más simple y algo más ligero. Elige WebSockets en Python cuando:

  • Tu red solo permite HTTP(S) saliente, por ejemplo un firewall corporativo o la red de un hotel que bloquea el 1883 y el 8883 (consulta MQTT detrás de un firewall corporativo).
  • El broker está detrás de un reverse proxy o balanceador que solo enruta HTTP, como Nginx o un ingress en la nube.
  • Quieres que un script de backend use exactamente el mismo endpoint que tu dashboard web, para reproducir un problema del navegador.

Si nada de esto aplica, TCP en 1883/8883 es la opción por defecto. La diferencia de overhead está medida en rendimiento TCP vs WebSocket.

WebSockets sin cifrar (ws://)

ws_client.py
import time

import paho.mqtt.client as mqtt


def on_connect(client, userdata, flags, reason_code, properties):
    print("CONNACK:", reason_code)
    client.subscribe("testmqtt/py-ws/#")


def on_message(client, userdata, msg):
    print(msg.topic, msg.payload.decode())


client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, transport="websockets")
client.ws_set_options(path="/mqtt")  # /mqtt es también el valor por defecto de paho
client.on_connect = on_connect
client.on_message = on_message

client.connect("broker.connectmq.com", 8083, keepalive=60)
client.loop_start()
client.publish("testmqtt/py-ws/hello", "hello over ws", qos=1)
time.sleep(2)
client.loop_stop()
client.disconnect()

transport debe definirse en el constructor; no puedes cambiar un cliente existente de TCP a WebSockets. Los valores válidos son "tcp", "websockets" y "unix"; cualquier otro lanza ValueError: transport must be "websockets", "tcp" or "unix", not ....

WebSockets seguros (wss://)

Para cualquier cosa más allá de una prueba rápida, usa wss. Es el mismo código más tls_set(), el puerto seguro y credenciales reales:

wss_client.py
import os

import paho.mqtt.client as mqtt

client = mqtt.Client(
    mqtt.CallbackAPIVersion.VERSION2,
    client_id="py-wss-demo",
    transport="websockets",
    protocol=mqtt.MQTTv5,
)
client.username_pw_set(os.environ["MQTT_USER"], os.environ["MQTT_PASS"])
client.tls_set()                     # verifica el certificado y el hostname
client.ws_set_options(path="/mqtt")

client.on_connect = lambda c, u, f, rc, p: print("CONNACK:", rc)
client.connect("broker.connectmq.com", 8084, keepalive=60)
client.loop_forever()

paho realiza primero el handshake TLS, luego el upgrade HTTP dentro del canal cifrado y después el CONNECT de MQTT. Todas las opciones y errores TLS de MQTT sobre TLS en Python se aplican sin cambios.

Paths y cabeceras personalizadas

El path del WebSocket debe coincidir con lo que espera el broker o el proxy, y no hay un estándar universal. /mqtt es el más común (es el valor por defecto en EMQX, HiveMQ y muchos brokers gestionados), el plugin Web MQTT de RabbitMQ usa /ws, y los reverse proxies pueden mapear el path que elijas. Un path incorrecto es la causa número uno de fallos de conexión WebSocket; consulta WebSocket path 404.

ws_set_options() también acepta cabeceras, ya sea como un dict que se fusiona con las predeterminadas o como una función que recibe las cabeceras por defecto y devuelve el conjunto final:

ws_headers.py
import os

TOKEN = os.environ["GATEWAY_TOKEN"]

def add_headers(headers):
    headers["Authorization"] = f"Bearer {TOKEN}"   # para un proxy de autenticación delante del broker
    headers["Origin"] = "https://dashboard.example.com"
    return headers

client.ws_set_options(path="/mqtt", headers=add_headers)

Por defecto paho envía Origin: http://host:port (o https:// con TLS) y Sec-WebSocket-Protocol: mqtt. Sobrescribir Origin importa cuando un proxy aplica una lista de orígenes permitidos, tema que se trata en CORS y comprobación de origen en WebSocket. Recuerda que las credenciales MQTT siguen yendo en el paquete CONNECT mediante username_pw_set(); las cabeceras solo sirven para componentes HTTP situados delante del broker.

Conectar a través de un proxy HTTP o SOCKS

paho atraviesa proxies con la librería PySocks. Instálala primero con pip install pysocks y configura el proxy explícitamente:

ws_via_proxy.py
import socks  # del paquete PySocks

import paho.mqtt.client as mqtt

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, transport="websockets")
client.tls_set()
client.proxy_set(proxy_type=socks.HTTP, proxy_addr="proxy.corp.local", proxy_port=3128)
client.connect("broker.connectmq.com", 8084)
client.loop_forever()

Con socks.HTTP, PySocks abre un túnel HTTP CONNECT hacia broker.connectmq.com:8084, y paho ejecuta TLS, el upgrade WebSocket y MQTT a través de él. Usa socks.SOCKS5 (opcionalmente con proxy_username / proxy_password) para un proxy SOCKS.

Dos detalles ahorran mucho tiempo:

  • paho ignora HTTPS_PROXY y HTTP_PROXY. La única variable de entorno que lee es mqtt_proxy, por ejemplo export mqtt_proxy=http://proxy.corp.local:3128 o socks://host:1080, y solo si PySocks está instalado. Los hosts en no_proxy se excluyen.
  • Muchos proxies corporativos solo permiten CONNECT al puerto 443. Si el túnel se rechaza, necesitas un endpoint del broker en el 443 o una excepción en el proxy.

Errores de handshake y cómo corregirlos

Los problemas de WebSocket aparecen como WebsocketConnectionError, una subclase de ConnectionError. Con connect() se lanza de inmediato; con connect_async() más loop_start() paho reintenta en silencio, así que activa client.enable_logger() mientras depuras.

ErrorCausa probableSolución
WebSocket handshake error, connection not upgradedRespuesta HTTP sin upgrade: path incorrecto (404), servidor web normal, página de error del proxyRevisa el path; asegúrate de que host y puerto apuntan al listener WebSocket
WebSocket handshake errorConexión cerrada durante el upgrade o respuesta que no es HTTP (a menudo ws sin cifrar contra un puerto TLS)Usa tls_set() con puertos wss; comprueba que el puerto es un listener WebSocket
WebSocket handshake error, invalid secret keyUn proxy o servidor devolvió un Sec-WebSocket-Accept incorrectoEvita o corrige el proxy que intercepta
[SSL: WRONG_VERSION_NUMBER] wrong version numbertls_set() contra un puerto ws sin cifrar como el 8083Usa el 8084 para wss, o quita tls_set()
ConnectionRefusedError: [Errno 61] Connection refused (111 en Linux)Nada escucha en ese puerto, o un firewall lo rechazaVerifica el puerto; prueba desde otra red
CONNACK Not authorized / Bad user name or passwordEl WebSocket funciona, falló la autenticación MQTTRevisa credenciales y ACL

Consulta los reason codes MQTT de la última fila con el buscador de reason codes.

Cómo probar la conexión

Empieza comprobando el upgrade HTTP sin Python. Un endpoint WebSocket sano responde 101 Switching Protocols:

Probar el upgrade WebSocket con curl
curl -i -N --max-time 5 \
  -H "Connection: Upgrade" -H "Upgrade: websocket" \
  -H "Sec-WebSocket-Version: 13" \
  -H "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==" \
  -H "Sec-WebSocket-Protocol: mqtt" \
  https://broker.connectmq.com:8084/mqtt

Un 404 significa que el path es incorrecto, un 400 o 426 suele indicar que se eliminó una cabecera, y un error TLS significa que estás hablando con el puerto equivocado. Después haz una prueba de extremo a extremo: abre el cliente MQTT online, conéctate por wss al mismo broker, suscríbete a testmqtt/py-ws/# y ejecuta ws_client.py. Si el navegador recibe tu mensaje, Python, el path y el broker están bien. En probar una conexión MQTT por WebSocket tienes más comprobaciones, incluidos proxies y balanceadores.

Suscríbete en el navegador y publica desde Python

Usar aiomqtt en su lugar

Si tu código se basa en asyncio, aiomqtt (que envuelve paho) expone las mismas opciones como argumentos del constructor:

aiomqtt_ws.py
import asyncio
import ssl

import aiomqtt


async def main():
    async with aiomqtt.Client(
        "broker.connectmq.com",
        8084,
        transport="websockets",
        websocket_path="/mqtt",
        tls_context=ssl.create_default_context(),
    ) as client:
        await client.subscribe("testmqtt/py-ws/#")
        await client.publish("testmqtt/py-ws/hello", "hello from aiomqtt")
        async for message in client.messages:
            print(message.topic, message.payload)
            break


asyncio.run(main())

MQTT sobre WebSockets en Python es un argumento del constructor más el puerto y el path correctos. Usa wss con la verificación activa, define el path de forma explícita para que nadie tenga que adivinarlo, configura los proxies con proxy_set() o mqtt_proxy y prueba el upgrade con curl antes de culpar a tu código.

Preguntas frecuentes

¿Qué puerto y qué path usa paho-mqtt para WebSockets?

paho usa el puerto que pases a connect() y, por defecto, el path /mqtt. Los WebSockets sin cifrar suelen estar en el 8083 o el 80, los seguros en el 8084 o el 443. Cambia el path con client.ws_set_options(path=...) si tu broker o tu proxy usan otro.

¿paho-mqtt respeta HTTPS_PROXY o HTTP_PROXY?

No. paho solo lee una variable de entorno llamada mqtt_proxy (con una URL http:// o socks://) y solo si PySocks está instalado. En los demás casos, configura el proxy explícitamente con client.proxy_set(proxy_type=socks.HTTP, proxy_addr=..., proxy_port=...).

¿Qué significa WebSocket handshake error, connection not upgraded?

El servidor respondió a la petición HTTP de upgrade pero no cambió a WebSocket. Normalmente el path es incorrecto (una página 404), has llegado a un servidor web o proxy normal en lugar del listener WebSocket del broker, o un proxy eliminó la cabecera Upgrade.