Tutoriais

MQTT sobre WebSockets em Python

Conecte o paho-mqtt via WebSockets: transport="websockets", wss com TLS, path e headers no ws_set_options(), proxies HTTP e correções de erros de handshake.

Atualizado · 8 min de leitura

MQTT sobre WebSockets em Python

Para usar MQTT sobre WebSockets em Python, crie o cliente paho com transport="websockets", conecte na porta WebSocket do broker e, se necessário, defina o path da URL com ws_set_options(). Para wss criptografado, adicione tls_set() e use a porta de WebSocket seguro. Fora isso, a API MQTT é idêntica: os mesmos callbacks, o mesmo publish() e o mesmo subscribe().

Este guia mostra conexões ws e wss, paths e headers personalizados, conexão por proxies HTTP e SOCKS e como depurar os erros de handshake que o paho lança.

Quando WebSockets fazem sentido no Python

Navegadores só falam MQTT via WebSockets, mas o Python pode usar TCP puro, que é mais simples e um pouco mais leve. Escolha WebSockets no Python quando:

  • A sua rede só permite HTTP(S) de saída, por exemplo um firewall corporativo ou a rede de um hotel que bloqueia 1883 e 8883 (veja MQTT atrás de um firewall corporativo).
  • O broker fica atrás de um reverse proxy ou load balancer que só roteia HTTP, como Nginx ou um ingress na nuvem.
  • Você quer que um script de backend use exatamente o mesmo endpoint do seu dashboard web, para reproduzir um problema do navegador.

Se nada disso se aplica, TCP em 1883/8883 é a escolha padrão. A diferença de overhead está medida em desempenho TCP vs WebSocket.

WebSockets sem criptografia (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 também é o padrão do 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()

O transport precisa ser definido no construtor; não dá para trocar um cliente existente de TCP para WebSockets. Os valores válidos são "tcp", "websockets" e "unix"; qualquer outro lança ValueError: transport must be "websockets", "tcp" or "unix", not ....

WebSockets seguros (wss://)

Para qualquer coisa além de um teste rápido, use wss. É o mesmo código mais tls_set(), a porta segura e credenciais reais:

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 o certificado e o 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()

O paho faz primeiro o handshake TLS, depois o upgrade HTTP dentro do canal criptografado e então o CONNECT do MQTT. Todas as opções e erros de TLS de MQTT sobre TLS em Python valem sem mudanças.

Paths e headers personalizados

O path do WebSocket precisa bater com o que o broker ou o proxy espera, e não existe padrão universal. /mqtt é o mais comum (é o padrão no EMQX, no HiveMQ e em muitos brokers gerenciados), o plugin Web MQTT do RabbitMQ usa /ws, e reverse proxies podem mapear qualquer path que você escolher. Path errado é a causa número um de falhas de conexão WebSocket; veja WebSocket path 404.

O ws_set_options() também aceita headers, seja como um dict mesclado aos padrões, seja como uma função que recebe os headers padrão e retorna o conjunto final:

ws_headers.py
import os

TOKEN = os.environ["GATEWAY_TOKEN"]

def add_headers(headers):
    headers["Authorization"] = f"Bearer {TOKEN}"   # para um proxy de autenticação na frente do broker
    headers["Origin"] = "https://dashboard.example.com"
    return headers

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

Por padrão o paho envia Origin: http://host:port (ou https:// com TLS) e Sec-WebSocket-Protocol: mqtt. Sobrescrever o Origin importa quando um proxy aplica uma lista de origens permitidas, assunto de CORS e verificação de origem no WebSocket. Lembre que as credenciais MQTT continuam indo no pacote CONNECT via username_pw_set(); headers servem apenas para componentes HTTP na frente do broker.

Conectando por um proxy HTTP ou SOCKS

O paho atravessa proxies com a biblioteca PySocks. Instale primeiro com pip install pysocks e configure o proxy explicitamente:

ws_via_proxy.py
import socks  # do pacote 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()

Com socks.HTTP, o PySocks abre um túnel HTTP CONNECT até broker.connectmq.com:8084, e o paho roda TLS, o upgrade WebSocket e o MQTT por dentro dele. Use socks.SOCKS5 (opcionalmente com proxy_username / proxy_password) para um proxy SOCKS.

Dois detalhes economizam muito tempo:

  • O paho ignora HTTPS_PROXY e HTTP_PROXY. A única variável de ambiente que ele lê é mqtt_proxy, por exemplo export mqtt_proxy=http://proxy.corp.local:3128 ou socks://host:1080, e só se o PySocks estiver instalado. Hosts em no_proxy são ignorados pelo proxy.
  • Muitos proxies corporativos só permitem CONNECT para a porta 443. Se o túnel for recusado, você precisa de um endpoint do broker na 443 ou de uma exceção no proxy.

Erros de handshake e como corrigi-los

Problemas de WebSocket aparecem como WebsocketConnectionError, uma subclasse de ConnectionError. Com connect() ele é lançado na hora; com connect_async() mais loop_start() o paho tenta de novo em silêncio, então ative client.enable_logger() enquanto depura.

ErroCausa provávelCorreção
WebSocket handshake error, connection not upgradedResposta HTTP sem upgrade: path errado (404), servidor web comum, página de erro do proxyConfira o path; garanta que host e porta apontam para o listener WebSocket
WebSocket handshake errorConexão fechada durante o upgrade ou resposta não HTTP (muitas vezes ws sem criptografia contra uma porta TLS)Use tls_set() com portas wss; confira se a porta é um listener WebSocket
WebSocket handshake error, invalid secret keyUm proxy ou servidor devolveu um Sec-WebSocket-Accept erradoContorne ou corrija o proxy que intercepta
[SSL: WRONG_VERSION_NUMBER] wrong version numbertls_set() contra uma porta ws sem criptografia, como a 8083Use a 8084 para wss, ou remova o tls_set()
ConnectionRefusedError: [Errno 61] Connection refused (111 no Linux)Nada escuta nessa porta, ou um firewall a rejeitaVerifique a porta; teste de outra rede
CONNACK Not authorized / Bad user name or passwordO WebSocket funciona, a autenticação MQTT falhouConfira credenciais e ACLs

Consulte os reason codes MQTT da última linha com o buscador de reason codes.

Como testar a conexão

Comece verificando o upgrade HTTP sem Python. Um endpoint WebSocket saudável responde 101 Switching Protocols:

Testar o upgrade WebSocket com 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

Um 404 significa path errado, um 400 ou 426 geralmente indica que um header foi removido, e um erro de TLS significa que você está falando com a porta errada. Depois faça uma checagem de ponta a ponta: abra o cliente MQTT online, conecte via wss ao mesmo broker, assine testmqtt/py-ws/# e rode o ws_client.py. Se o navegador receber sua mensagem, Python, path e broker estão corretos. Em testar uma conexão MQTT via WebSocket há mais checagens, incluindo proxies e load balancers.

Assine no navegador e publique a partir do Python

Usando aiomqtt

Se o seu código é baseado em asyncio, o aiomqtt (que encapsula o paho) expõe as mesmas opções como argumentos do construtor:

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 em Python é um argumento no construtor mais a porta e o path certos. Use wss com verificação ativa, defina o path explicitamente para ninguém ter que adivinhar, configure proxies com proxy_set() ou mqtt_proxy e teste o upgrade com curl antes de culpar o seu código.

Perguntas frequentes

Qual porta e qual path o paho-mqtt usa para WebSockets?

O paho usa a porta que você passa em connect() e, por padrão, o path /mqtt. WebSockets sem criptografia costumam ficar na 8083 ou 80, os seguros na 8084 ou 443. Mude o path com client.ws_set_options(path=...) se o seu broker ou proxy usar outro.

O paho-mqtt respeita HTTPS_PROXY ou HTTP_PROXY?

Não. O paho só lê uma variável de ambiente chamada mqtt_proxy (com uma URL http:// ou socks://) e apenas se o PySocks estiver instalado. Fora isso, configure o proxy explicitamente com client.proxy_set(proxy_type=socks.HTTP, proxy_addr=..., proxy_port=...).

O que significa WebSocket handshake error, connection not upgraded?

O servidor respondeu à requisição HTTP de upgrade, mas não mudou para WebSocket. Normalmente o path está errado (uma página 404), você chegou a um servidor web ou proxy comum em vez do listener WebSocket do broker, ou um proxy removeu o header Upgrade.