MQTTnet es la biblioteca MQTT más utilizada en .NET y cubre desde una prueba rápida en consola hasta un servicio en producción. En este tutorial crearás un publicador y un suscriptor en C# con MQTTnet, usarás propiedades de MQTT 5, cifrarás la conexión con TLS y añadirás una lógica de reconexión fiable. Todos los ejemplos se ejecutan contra el servidor MQTT público gratuito broker.connectmq.com, así que puedes seguirlo sin instalar nada más que el SDK de .NET.
Qué necesitas
- SDK de .NET 8 o posterior (compruébalo con
dotnet --version) - Cualquier editor: Visual Studio, Rider o VS Code con la extensión de C#
- Una terminal con acceso saliente al puerto 8883 (TLS) o 1883 (TCP sin cifrar)
No necesitas cuenta para la primera parte. El servidor público en broker.connectmq.com acepta clientes anónimos con cualquier client ID único y no vacío. Recuerda que el área pública la puede leer cualquiera, así que usa un prefijo de topic aleatorio y nunca envíes datos reales.
Versiones de MQTTnet: lee esto primero
MQTTnet hizo una gran limpieza de su API en la versión 5. Si copias fragmentos de artículos antiguos, te encontrarás con errores de compilación. Las diferencias más importantes:
| Tema | MQTTnet 4.x | MQTTnet 5.x |
|---|---|---|
| Clase factoría | MqttFactory |
MqttClientFactory |
| Espacios de nombres | MQTTnet.Client, MQTTnet |
Casi todo unificado en MQTTnet |
| Cliente con reconexión automática (ManagedClient) | MQTTnet.Extensions.ManagedClient |
Eliminado; escribe tu propio bucle |
| Tipo de payload | byte[] / ArraySegment<byte> |
ReadOnlySequence<byte> |
| Configuración de TLS | WithTls() (obsoleto) o WithTlsOptions |
WithTlsOptions(o => o.UseTls()) |
Esta guía se centra en MQTTnet 5. Los conceptos se trasladan a la 4.x, pero los nombres de las clases cambian como se muestra arriba. El repositorio oficial de MQTTnet tiene ejemplos de cada funcionalidad si necesitas profundizar.
Crea los proyectos
Crea una solución con dos aplicaciones de consola:
mkdir MqttNetDemo && cd MqttNetDemo
dotnet new console -n Publisher
dotnet new console -n Subscriber
dotnet add Publisher package MQTTnet
dotnet add Subscriber package MQTTnet
Elige ahora un prefijo de topic único y úsalo en ambas aplicaciones. Algo como connectmq/demo/k7f3q9/ sirve. Las colisiones en un broker compartido son la fuente clásica de la confusión «¿por qué recibo mensajes que nunca he enviado?».
Tu primer suscriptor con MQTTnet
Sustituye Subscriber/Program.cs por el código siguiente. Se conecta por TLS en el puerto 8883, usa MQTT 5, se suscribe con QoS 1 e imprime cada mensaje junto con sus propiedades de MQTT 5.
using System.Text;
using MQTTnet;
using MQTTnet.Formatter;
using MQTTnet.Protocol;
const string Prefix = "connectmq/demo/k7f3q9";
var factory = new MqttClientFactory();
using var client = factory.CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("broker.connectmq.com", 8883)
.WithClientId($"sub-{Guid.NewGuid():N}")
.WithProtocolVersion(MqttProtocolVersion.V500)
.WithTlsOptions(o => o.UseTls())
.WithCleanStart(true)
.WithKeepAlivePeriod(TimeSpan.FromSeconds(30))
.Build();
client.ApplicationMessageReceivedAsync += e =>
{
var msg = e.ApplicationMessage;
var payload = msg.ConvertPayloadToString();
Console.WriteLine($"[{DateTime.Now:HH:mm:ss}] {msg.Topic} (QoS {(int)msg.QualityOfServiceLevel}, retain={msg.Retain})");
Console.WriteLine($" payload: {payload}");
if (!string.IsNullOrEmpty(msg.ContentType))
Console.WriteLine($" content-type: {msg.ContentType}");
foreach (var p in msg.UserProperties ?? [])
Console.WriteLine($" user-property: {p.Name}={p.Value}");
return Task.CompletedTask;
};
var result = await client.ConnectAsync(options, CancellationToken.None);
Console.WriteLine($"Connected: {result.ResultCode}");
var subscribe = new MqttClientSubscribeOptionsBuilder()
.WithTopicFilter(f => f
.WithTopic($"{Prefix}/#")
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce))
.Build();
var subResult = await client.SubscribeAsync(subscribe, CancellationToken.None);
foreach (var item in subResult.Items)
Console.WriteLine($"Subscribed {item.TopicFilter.Topic}: {item.ResultCode}");
Console.WriteLine("Waiting for messages. Press Enter to exit.");
Console.ReadLine();
await client.DisconnectAsync(new MqttClientDisconnectOptionsBuilder()
.WithReason(MqttClientDisconnectOptionsReason.NormalDisconnection)
.Build());
Ejecútalo con dotnet run --project Subscriber. Deberías ver Connected: Success y un QoS concedido para la suscripción. Fíjate en que el handler se registra antes de ConnectAsync; si te suscribes con una sesión persistente, los mensajes pueden llegar en el mismo instante en que se abre la conexión, y un handler registrado tarde se los perdería.
Publicar con MQTTnet y propiedades de MQTT 5
Ahora Publisher/Program.cs. Envía una lectura JSON cada dos segundos con un content type, una propiedad de usuario y un intervalo de expiración del mensaje, además de un mensaje de estado retenido.
using System.Text.Json;
using MQTTnet;
using MQTTnet.Formatter;
using MQTTnet.Protocol;
const string Prefix = "connectmq/demo/k7f3q9";
var factory = new MqttClientFactory();
using var client = factory.CreateMqttClient();
var options = new MqttClientOptionsBuilder()
.WithTcpServer("broker.connectmq.com", 8883)
.WithClientId($"pub-{Guid.NewGuid():N}")
.WithProtocolVersion(MqttProtocolVersion.V500)
.WithTlsOptions(o => o.UseTls())
.WithWillTopic($"{Prefix}/sensor-1/status")
.WithWillPayload("offline")
.WithWillRetain(true)
.WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.Build();
await client.ConnectAsync(options, CancellationToken.None);
// Estado "online" retenido: los nuevos suscriptores lo ven de inmediato.
await client.PublishAsync(new MqttApplicationMessageBuilder()
.WithTopic($"{Prefix}/sensor-1/status")
.WithPayload("online")
.WithRetainFlag(true)
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.Build());
var rnd = new Random();
using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, e) => { e.Cancel = true; cts.Cancel(); };
while (!cts.IsCancellationRequested)
{
var reading = new
{
deviceId = "sensor-1",
temperature = Math.Round(20 + rnd.NextDouble() * 5, 2),
ts = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds()
};
var message = new MqttApplicationMessageBuilder()
.WithTopic($"{Prefix}/sensor-1/telemetry")
.WithPayload(JsonSerializer.Serialize(reading))
.WithContentType("application/json")
.WithUserProperty("firmware", "1.4.2")
.WithMessageExpiryInterval(60)
.WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
.Build();
var pub = await client.PublishAsync(message, cts.Token);
Console.WriteLine($"Published {reading.temperature} -> {pub.ReasonCode}");
try { await Task.Delay(2000, cts.Token); } catch (TaskCanceledException) { }
}
await client.DisconnectAsync();
Arranca primero el suscriptor y después dotnet run --project Publisher. El suscriptor imprime el JSON, el content type application/json y la propiedad de usuario firmware. Detén el publicador con Ctrl+C y el estado retenido sigue en online porque la desconexión fue limpia. Si en cambio matas el proceso (cerrando la ventana de la terminal), el broker publica el Last Will y cambia el estado retenido a offline. Ese patrón es el indicador de presencia fiable más sencillo de MQTT.
El resultado de PublishAsync contiene un reason code para QoS 1 y 2. Con MQTT 5 puedes leer códigos significativos como QuotaExceeded (0x97) en lugar de un fallo silencioso, algo que merece la pena registrar en cualquier aplicación real. Si quieres repasar qué garantiza cada nivel, consulta los niveles de QoS de MQTT explicados.
Reconexión sin ManagedClient
Como MQTTnet 5 eliminó ManagedClient, la reconexión es cosa tuya. Un patrón pequeño y robusto es un bucle en segundo plano que comprueba IsConnected y usa TryPingAsync para detectar conexiones medio abiertas:
async Task KeepConnectedAsync(IMqttClient client, MqttClientOptions options,
Func<Task> onConnected, CancellationToken ct)
{
var delay = TimeSpan.FromSeconds(1);
while (!ct.IsCancellationRequested)
{
try
{
if (!await client.TryPingAsync(ct))
{
await client.ConnectAsync(options, ct);
await onConnected(); // vuelve a suscribirte aquí
Console.WriteLine("Reconnected");
delay = TimeSpan.FromSeconds(1);
}
}
catch (Exception ex)
{
Console.WriteLine($"Connect failed: {ex.Message}");
delay = TimeSpan.FromSeconds(Math.Min(delay.TotalSeconds * 2, 60));
}
await Task.Delay(delay + TimeSpan.FromMilliseconds(Random.Shared.Next(0, 500)), ct);
}
}
Aquí importan tres detalles. Primero, vuelve a suscribirte en onConnected siempre que la sesión no se haya reanudado; si usas sesiones persistentes, comprueba result.IsSessionPresent en el resultado de ConnectAsync. Segundo, el backoff exponencial con jitter evita que una flota de clientes se reconecte al unísono tras un corte de red. Tercero, mantén estable el client ID entre reconexiones si dependes de una sesión persistente; genéralo una vez al arrancar, no dentro del bucle.
Errores habituales de MQTTnet y cómo solucionarlos
| Síntoma | Causa probable | Solución |
|---|---|---|
MqttCommunicationTimedOutException al conectar |
Puerto 8883 o 1883 bloqueado por un firewall o un proxy corporativo | Prueba con Test-NetConnection broker.connectmq.com -Port 8883; intenta con el otro puerto |
AuthenticationException durante el handshake TLS |
Almacén raíz del sistema antiguo o reloj del sistema desajustado | Actualiza el trust store del sistema y sincroniza la hora; el broker usa un certificado de Let's Encrypt |
La conexión devuelve NotAuthorized / BadUserNameOrPassword |
Credenciales incorrectas o client ID distinto del vinculado a las credenciales | Copia exactamente el usuario, la contraseña y el client ID desde el portal |
| El handler nunca se ejecuta | Handler registrado después de suscribirse o errata en el filtro de topic | Regístralo antes de conectar; imprime subResult.Items |
| El cliente se desconecta cada pocos segundos | Dos procesos usan el mismo client ID | El broker expulsa la sesión más antigua; haz que los ID sean únicos |
QuotaExceeded (0x97) |
Se alcanzó un límite del plan o del broker público | Reduce el ritmo o pasa a un plan con límites más altos |
Ten en cuenta los límites del broker público mientras pruebas: 64 KB por mensaje, 10 mensajes por segundo por cliente, 10 suscripciones por cliente y hasta 8 niveles de topic. El publicador anterior está muy por debajo de ellos.
Paso a un broker privado
El broker público es ideal para experimentar, pero el tráfico de producción debe ir a una instancia privada con credenciales. Con un broker MQTT gestionado de ConnectMQ, te registras, eliges un plan, creas un broker y recibes un usuario, una contraseña y un client ID generados. El cambio en el código son tres líneas:
var options = new MqttClientOptionsBuilder()
.WithTcpServer("YOUR_BROKER_HOST", 8883)
.WithClientId("YOUR_CLIENT_ID")
.WithCredentials("YOUR_USERNAME", "YOUR_PASSWORD")
.WithProtocolVersion(MqttProtocolVersion.V500)
.WithTlsOptions(o => o.UseTls())
.Build();
Mantén las credenciales fuera del código fuente: léelas de variables de entorno, de dotnet user-secrets durante el desarrollo o del gestor de secretos de tu plataforma en producción. En ConnectMQ, los client ID están vinculados a las credenciales emitidas, así que una contraseña filtrada no basta por sí sola para suplantar a otro cliente, pero aun así debes rotar cualquier cosa que acabe en un repositorio.
El plan Serverless gratuito permite hasta 25 conexiones, QoS hasta 1 y mensajes de 256 KB, más que suficiente para desarrollo. Serverless de pago lo amplía a 1.000 conexiones con QoS 2, mensajes de 1 MB y suscripciones compartidas, y Dedicated te ofrece un clúster aislado.
Próximos pasos con MQTTnet
Cuando las aplicaciones de consola funcionen, el mismo cliente encaja en aplicaciones .NET más grandes:
- Aloja el suscriptor como un servicio de larga duración con inyección de dependencias: consulta la guía del servicio en segundo plano MQTT con ASP.NET Core.
- Crea una interfaz multiplataforma para móvil y escritorio con el tutorial de la aplicación MQTT con .NET MAUI.
- Controla escenas 3D con datos en vivo mediante un cliente MQTT en Unity.
- Compara MQTTnet con bibliotecas de otros lenguajes en la visión general de bibliotecas cliente MQTT.
Si todavía estás decidiendo a dónde deben conectarse estos clientes, la guía para elegir un broker MQTT en la nube explica las ventajas e inconvenientes de alojarlo tú mismo frente a un broker MQTT gestionado.
Empieza con ConnectMQ
Tu código MQTTnet está listo para producción en cuanto apunta a un broker privado. Crea uno en app.connectmq.com con el plan Serverless gratuito, sin tarjeta de crédito, copia las credenciales generadas en el builder de opciones y observa cómo llegan tus mensajes al explorador de topics en vivo del portal y al cliente MQTT integrado en el navegador.
Preguntas frecuentes
¿MQTTnet es gratuito para uso comercial?
Sí. MQTTnet es de código abierto bajo la licencia MIT y se mantiene dentro de la .NET Foundation, así que puedes usarlo en productos comerciales.
¿MQTTnet admite MQTT 5?
Sí. Configura WithProtocolVersion(MqttProtocolVersion.V500) para usar funciones de MQTT 5 como las propiedades de usuario, el content type, la expiración de mensajes, los reason codes y la expiración de sesión. ConnectMQ admite tanto MQTT 3.1.1 como 5.0.
¿Qué pasó con ManagedClient de MQTTnet?
Se eliminó en MQTTnet 5. Usa un bucle de reconexión como el de este tutorial o quédate en MQTTnet 4.x si dependes mucho de ManagedClient y todavía no puedes migrar.
¿MQTTnet puede conectarse por WebSocket?
Sí. Usa WithWebSocketServer(o => o.WithUri("wss://broker.connectmq.com:8084/mqtt")) en lugar de WithTcpServer. Resulta útil cuando en una red solo se permite el tráfico HTTPS saliente.



