Tutoriales

Enviar archivos e imágenes por MQTT: fragmentación, base64 y reensamblado

Cómo enviar archivos e imágenes por MQTT: binario frente a base64, límites de tamaño del broker, fragmentación con manifiesto y reensamblado con SHA-256.

Actualizado · 9 min de lectura

Enviar archivos e imágenes por MQTT: fragmentación, base64 y reensamblado

Sí, puedes enviar archivos e imágenes por MQTT: un payload son solo bytes, así que un JPEG o un archivo de log se pueden publicar exactamente como están en disco. El trabajo está en los detalles: no superar el límite de tamaño del broker, dividir los archivos grandes en fragmentos (chunks), volver a unirlos en el orden correcto y demostrar que el resultado es idéntico al original.

Este tutorial explica cuándo enviar binario puro y cuándo tiene sentido base64, un protocolo de fragmentación sencillo con un manifiesto, y un emisor y un receptor en Python que puedes probar en pocos minutos.

¿Binario o base64?

A MQTT no le importa lo que hay dentro del payload. No necesitas codificar una imagen antes de publicarla; todas las librerías cliente habituales aceptan bytes sin procesar (bytes en Python, Buffer / Uint8Array en MQTT.js, byte[] en Java y .NET).

Base64 es una opción que solo eliges cuando los bytes deben vivir dentro de un documento de texto:

EnfoqueSobrecargaÚsalo cuando
Payload binario puro0 %Por defecto. El receptor sabe que el topic transporta un archivo.
Base64 dentro de JSONaprox. 33 % más las claves JSONUn consumidor solo acepta texto (algunas herramientas low-code, puentes de webhooks, pipelines de logs).
Payload binario + propiedades MQTT 5unos pocos bytesQuieres metadatos (nombre de archivo, tipo MIME) sin envolver los bytes.

Una foto de 600 KB ocupa unos 800 KB una vez codificada en base64, y el receptor tiene que parsear JSON y decodificar antes de poder usarla. Si controlas ambos extremos, envía binario.

Con MQTT 5 puedes adjuntar metadatos a un payload binario en lugar de envolverlo: define la propiedad Content Type como image/jpeg y pon el nombre del archivo en una user property. Deja el Payload Format Indicator en 0 (bytes sin especificar); ponerlo a 1 indica al broker y al receptor que el payload es texto UTF-8, y un JPEG no lo es.

Conoce tus límites de tamaño

La especificación MQTT permite una remaining length de hasta 268.435.455 bytes, unos 256 MB por paquete. En la práctica nadie se acerca a eso, porque los brokers y servicios imponen su propio máximo:

Broker o servicioLímite típico
MosquittoSin límite por defecto; se define con max_packet_size / message_size_limit
EMQX1 MB por defecto (mqtt.max_packet_size), configurable
HiveMQmax-packet-size configurable, por defecto cerca del máximo del protocolo
AWS IoT Core128 KB por mensaje
Azure IoT Hub256 KB por mensaje dispositivo-a-nube

Los límites cambian entre versiones y planes, así que toma la tabla como punto de partida y compruébalo con tu propio broker. En MQTT 5 el servidor anuncia su límite en la propiedad Maximum Packet Size del CONNACK; un publish que lo supere suele terminar en una desconexión con el reason code 0x95 (Packet too large). Los brokers MQTT 3.1.1 normalmente cierran la conexión sin más, lo que parece una desconexión aleatoria. La guía de packet too large recorre esos síntomas, y la calculadora de tamaño de paquete muestra cuánto suman el topic y las propiedades al tamaño del payload.

Los mensajes grandes también hacen daño de formas menos evidentes: bloquean la conexión TCP mientras están en tránsito (los pings de keep-alive y otros mensajes esperan detrás), el broker debe guardarlos en memoria para cada sesión offline que los tenga en cola y, en un enlace con pérdidas, un solo publish fallido de 5 MB se retransmite entero.

Un protocolo de fragmentación sencillo

Para todo lo que supere un mensaje individual cómodo (una buena regla es 64–256 KB), divide el archivo en fragmentos. Mantén el protocolo aburrido:

  1. Manifiesto en testmqtt/files/<device>/<file_id>/meta: JSON con nombre del archivo, tamaño total, tamaño de fragmento, número de fragmentos y un hash SHA-256.
  2. Fragmentos en testmqtt/files/<device>/<file_id>/chunk/<index>: bytes sin procesar, sin codificación.
  3. El receptor reúne los fragmentos por índice y, cuando los tiene todos, los une y compara el hash.

¿Por qué el índice en el topic? MQTT solo garantiza el orden por topic desde un mismo publicador, así que los fragmentos en topics distintos pueden entregarse en cualquier orden. Con QoS 1 un fragmento también puede llegar dos veces; guardar los fragmentos en un mapa indexado hace que los duplicados sean inofensivos. Poner el índice en el topic mantiene el payload como datos puros del archivo; también podrías anteponer a cada fragmento una cabecera binaria de 4 bytes si prefieres un único topic por archivo.

Usa QoS 1 para los fragmentos. QoS 0 acabará perdiendo alguno en una red mala, y un archivo al que le falta un fragmento no sirve. QoS 2 añade un viaje de ida y vuelta por fragmento sin mucho beneficio, porque el receptor ya elimina duplicados.

El emisor (Python)

Instala el cliente con pip install "paho-mqtt>=2". El emisor espera cada PUBACK antes de enviar el siguiente fragmento, lo que te da control de flujo natural y evita desbordar la ventana inflight del broker:

send_file.py
import hashlib, json, math, os, sys, time, uuid
import paho.mqtt.client as mqtt

HOST, PORT = "broker.connectmq.com", 1883
CHUNK_SIZE = 32 * 1024                      # 32 KB por mensaje
path = sys.argv[1]

with open(path, "rb") as f:
    data = f.read()

file_id = uuid.uuid4().hex[:12]
base = f"testmqtt/files/demo/{file_id}"
total = math.ceil(len(data) / CHUNK_SIZE)

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2, client_id=f"sender-{file_id}")
# client.username_pw_set("username", "password")
client.connect(HOST, PORT)
client.loop_start()

manifest = {
    "name": os.path.basename(path),
    "size": len(data),
    "chunk_size": CHUNK_SIZE,
    "chunks": total,
    "sha256": hashlib.sha256(data).hexdigest(),
}
client.publish(f"{base}/meta", json.dumps(manifest), qos=1).wait_for_publish(10)

for i in range(total):
    chunk = data[i * CHUNK_SIZE:(i + 1) * CHUNK_SIZE]
    info = client.publish(f"{base}/chunk/{i}", chunk, qos=1)
    info.wait_for_publish(10)               # esperar el PUBACK
    time.sleep(0.02)                        # muy por debajo de los límites compartidos

print(f"enviado {manifest['name']} en {total} fragmentos, id {file_id}")
client.loop_stop()
client.disconnect()

El receptor (Python)

El receptor se suscribe a todos los archivos del dispositivo, guarda los fragmentos a medida que llegan y escribe el archivo cuando el hash coincide:

receive_files.py
import hashlib, json
import paho.mqtt.client as mqtt

HOST, PORT = "broker.connectmq.com", 1883
transfers = {}                              # file_id -> {"meta": ..., "chunks": {}}

def on_connect(client, userdata, flags, reason_code, properties):
    client.subscribe("testmqtt/files/demo/+/#", qos=1)

def on_message(client, userdata, msg):
    parts = msg.topic.split("/")            # testmqtt/files/demo/<id>/meta|chunk/<n>
    file_id, kind = parts[3], parts[4]
    t = transfers.setdefault(file_id, {"meta": None, "chunks": {}})

    if kind == "meta":
        t["meta"] = json.loads(msg.payload)
    elif kind == "chunk":
        t["chunks"][int(parts[5])] = msg.payload   # los duplicados se sobrescriben

    meta = t["meta"]
    if meta and len(t["chunks"]) == meta["chunks"]:
        blob = b"".join(t["chunks"][i] for i in range(meta["chunks"]))
        if hashlib.sha256(blob).hexdigest() == meta["sha256"]:
            with open("received-" + meta["name"], "wb") as f:
                f.write(blob)
            print(f"OK  {meta['name']} ({len(blob)} bytes)")
        else:
            print(f"MAL {meta['name']}: el hash no coincide")
        del transfers[file_id]

client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.on_connect = on_connect
client.on_message = on_message
client.connect(HOST, PORT)
client.loop_forever()

Los fragmentos pueden llegar antes que el manifiesto; por eso la comprobación de completitud se ejecuta tras cada mensaje y no solo tras el manifiesto. En producción, añade un timeout que descarte las transferencias incompletas tras uno o dos minutos; de lo contrario, un manifiesto perdido provoca una fuga de memoria.

Probar la transferencia

Ejecuta el receptor en una terminal y el emisor en otra:

Prueba de ida y vuelta
python receive_files.py
# segunda terminal
python send_file.py photo.jpg
sha256sum photo.jpg received-photo.jpg     # macOS: shasum -a 256

Ambos hashes deben coincidir. Después ponlo más difícil: envía un archivo cuyo tamaño sea un múltiplo exacto del tamaño de fragmento (el clásico off-by-one), envía un archivo vacío y corta la red a mitad de transferencia para confirmar que el receptor informa de un archivo incompleto en lugar de escribir basura. Mientras corre una prueba, suscríbete a testmqtt/files/demo/+/meta en el cliente MQTT online para ver llegar los manifiestos.

Para archivos pequeños de un solo mensaje bastan los clientes de línea de comandos de Mosquitto. -f publica un archivo como payload, y -N evita que mosquitto_sub añada un salto de línea que corrompería el binario:

Imagen en un solo mensaje con mosquitto
mosquitto_sub -h broker.connectmq.com -t testmqtt/files/cli/snapshot -C 1 -N > out.jpg &
mosquitto_pub -h broker.connectmq.com -t testmqtt/files/cli/snapshot -q 1 -f snapshot.jpg

Tienes más patrones de línea de comandos en los ejemplos de mosquitto_pub y mosquitto_sub. Para averiguar a partir de qué tamaño tu broker rechaza mensajes, sigue la prueba de payloads grandes.

Mostrar una imagen en el navegador

Un dashboard puede mostrar una captura de cámara directamente desde MQTT sobre WebSockets seguros con MQTT.js 5. El payload llega como bytes, así que envuélvelo en un Blob:

snapshot.js
import mqtt from "mqtt";

const client = mqtt.connect("wss://broker.connectmq.com:8084/mqtt");
const img = document.querySelector("#snapshot");

client.on("connect", () => client.subscribe("testmqtt/files/cli/snapshot"));
client.on("message", (topic, payload) => {
  URL.revokeObjectURL(img.src);
  img.src = URL.createObjectURL(new Blob([payload], { type: "image/jpeg" }));
});

Mantén pequeñas las imágenes para el navegador (decenas de KB): cada pestaña suscrita descarga cada fotograma.

Cuándo no usar MQTT para archivos

MQTT es un protocolo de mensajería, no un servidor de archivos. Para imágenes de firmware, clips de vídeo o cualquier cosa por encima de unos pocos megabytes, el mejor patrón es subir el archivo a un almacenamiento de objetos y publicar solo un mensaje pequeño con la URL, el tamaño y el hash. El dispositivo descarga por HTTPS, que admite reanudación y caché en CDN, y MQTT hace lo que mejor sabe hacer: avisar al dispositivo de que hay algo que descargar. Las actualizaciones OTA de firmware por MQTT usan exactamente esta división, y las buenas prácticas de diseño de payloads explican cómo mantener ligero el mensaje de notificación.

Observa las transferencias en el cliente online

Enviar archivos por MQTT se reduce a cuatro reglas: envía bytes sin procesar, quédate por debajo del límite del broker, numera tus fragmentos y verifica un hash al final. Construye primero la prueba de ida y vuelta, y los casos límite (duplicados, fragmentos desordenados, un manifiesto perdido) serán fáciles de ver y corregir.

Preguntas frecuentes

¿Puede MQTT enviar archivos binarios?

Sí. Un payload MQTT es una secuencia arbitraria de bytes, así que puedes publicar un JPEG, un PDF o una imagen de firmware tal cual. Base64 solo hace falta cuando el payload tiene que viajar dentro de un formato de texto como JSON.

¿Cuál es el tamaño máximo de archivo en MQTT?

El protocolo permite paquetes de unos 256 MB, pero los brokers y servicios cloud suelen imponer límites mucho menores, a menudo entre 128 KB y unos pocos MB. Consulta el tamaño máximo de paquete de tu broker y fragmenta todo lo que sea mayor.

¿Debería enviar archivos grandes por MQTT?

Para imágenes y archivos de hasta unos pocos megabytes, la transferencia fragmentada por MQTT funciona bien. Para firmware o vídeo grandes, publica una URL de descarga por MQTT y deja que el dispositivo descargue el archivo por HTTPS desde un almacenamiento de objetos.