Tutoriais

MQTT em C# com MQTTnet 4 e 5: conectar, assinar, reconectar

Crie um cliente MQTT em C# com MQTTnet 4 e 5: conectar, assinar, publicar, TLS e WebSockets, um loop de reconexão que substitui o ManagedMqttClient e MQTT 5.

Atualizado · 9 min de leitura

MQTT em C# com MQTTnet 4 e 5: conectar, assinar, reconectar

MQTTnet é a biblioteca MQTT padrão do .NET. Para criar um cliente em C#, obtenha um IMqttClient da factory, descreva a conexão com MqttClientOptionsBuilder, registre um handler ApplicationMessageReceivedAsync e chame ConnectAsync, SubscribeAsync e PublishAsync. Desde o MQTTnet 5 não existe mais managed client, então a reconexão é por sua conta — são umas vinte linhas.

Este tutorial usa o MQTTnet 5 e aponta cada lugar em que a versão 4 é diferente, para que você possa segui-lo com qualquer uma. Todos os exemplos conectam em broker.connectmq.com e usam tópicos sob testmqtt/dotnet/.

Instalar e escolher a versão

Adicionar o MQTTnet a um projeto console
dotnet new console -n MqttDemo && cd MqttDemo
dotnet add package MQTTnet

Isso instala a última versão major. Se você mantém um código existente com MQTTnet 4, fixe-o com --version 4.* até estar pronto para migrar. As diferenças que você encontra primeiro:

TemaMQTTnet 4MQTTnet 5
Factorynew MqttFactory()new MqttClientFactory()
NamespacesMQTTnet, MQTTnet.ClientMQTTnet
Reconexão automáticaManagedMqttClient (pacote separado)Não incluída — escreva um loop de reconexão
Tipo do payloadArraySegment<byte> (PayloadSegment)ReadOnlySequence<byte> (Payload)
Builders, eventos, opções MQTT 5—Praticamente iguais

ConvertPayloadToString() funciona nas duas, por isso os exemplos usam esse método.

Conectar

Program.cs — conectar
using MQTTnet;            // v4: adicione também `using MQTTnet.Client;`
using MQTTnet.Formatter;
using MQTTnet.Protocol;

var factory = new MqttClientFactory();   // v4: new MqttFactory()
using var client = factory.CreateMqttClient();

var options = new MqttClientOptionsBuilder()
    .WithTcpServer("broker.connectmq.com", 1883)
    .WithClientId($"dotnet-{Guid.NewGuid():N}"[..20])
    .WithCredentials(Environment.GetEnvironmentVariable("MQTT_USER"),
                     Environment.GetEnvironmentVariable("MQTT_PASS"))
    .WithProtocolVersion(MqttProtocolVersion.V500)
    .WithKeepAlivePeriod(TimeSpan.FromSeconds(30))
    .WithCleanStart()
    .Build();

var result = await client.ConnectAsync(options, CancellationToken.None);
Console.WriteLine($"Conectado: {result.ResultCode}");

No endpoint compartilhado de testes você pode omitir WithCredentials; no seu próprio broker, passe usuário e senha por variáveis de ambiente em vez de deixá-los no código. Se o broker recusar a conexão, ConnectAsync lança MqttClientConnectingFailedException; o ResultCode dela (por exemplo BadUserNameOrPassword ou NotAuthorized) diz o motivo. A referência de reason codes MQTT lista todos os valores, e as exceções mais comuns são explicadas em erros do MQTTnet.

Adicionar um Last Will

A maioria dos clientes deveria avisar quando some. Adicione uma mensagem will ao mesmo builder de opções:

Last Will no builder de opções
    .WithWillTopic("testmqtt/dotnet/status")
    .WithWillPayload("offline")
    .WithWillQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
    .WithWillRetain()

O broker publica o will apenas quando a conexão termina sem um pacote DISCONNECT: um crash, uma queda de rede ou um timeout de keep-alive. Um DisconnectAsync() limpo o suprime, então publique você mesmo um status retido online após conectar e um offline antes de um desligamento planejado. Com MQTT 5, .WithWillDelayInterval(10) dá ao cliente alguns segundos para voltar antes de o will ser enviado, o que evita status piscando em quedas curtas de rede. MQTT Last Will cobre os detalhes.

Assinar e tratar mensagens

Registre o handler antes de assinar; caso contrário, mensagens retidas entregues logo após o SUBACK podem chegar antes de alguém estar escutando:

Receber mensagens
client.ApplicationMessageReceivedAsync += e =>
{
    var msg = e.ApplicationMessage;
    Console.WriteLine($"{msg.Topic} (QoS {(int)msg.QualityOfServiceLevel}): {msg.ConvertPayloadToString()}");
    return Task.CompletedTask;
};

var subscribe = factory.CreateSubscribeOptionsBuilder()
    .WithTopicFilter(f => f.WithTopic("testmqtt/dotnet/sensors/#")
                           .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce))
    .Build();

var subResult = await client.SubscribeAsync(subscribe, CancellationToken.None);
foreach (var item in subResult.Items)
    Console.WriteLine($"{item.TopicFilter.Topic}: {item.ResultCode}");

Sempre imprima o resultado do subscribe. Um broker que nega uma assinatura pela ACL não fecha a conexão; ele apenas devolve um código de falha como NotAuthorized no SUBACK, e seu handler nunca dispara. Verificar o ResultCode transforma uma falha silenciosa numa linha de log.

O handler roda no dispatcher interno do MQTTnet. Mantenha-o curto: faça o parse, repasse o trabalho (para um Channel<T>, uma fila ou uma task em segundo plano) e retorne. Trabalho demorado dentro do handler atrasa os acknowledgements e todas as mensagens que vêm atrás.

Publicar

Publicar uma leitura JSON
using System.Text.Json;

var payload = JsonSerializer.Serialize(new { celsius = 21.5, ts = DateTimeOffset.UtcNow });

var message = new MqttApplicationMessageBuilder()
    .WithTopic("testmqtt/dotnet/sensors/lab")
    .WithPayload(payload)
    .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtLeastOnce)
    .WithRetainFlag(false)
    .WithContentType("application/json")      // apenas MQTT 5
    .WithUserProperty("source", "mqttnet-demo") // apenas MQTT 5
    .Build();

var pub = await client.PublishAsync(message, CancellationToken.None);
Console.WriteLine($"Publish: {pub.ReasonCode}");

Com QoS 1, PublishAsync conclui quando o PUBACK chega, então o reason code é a resposta do broker. Com QoS 0 conclui assim que o pacote é escrito no socket. O broker pode rejeitar payloads grandes pelo tamanho máximo de pacote; a calculadora de tamanho de pacote mostra o tamanho real de uma mensagem na rede.

TLS e WebSockets

Troque o transporte no builder de opções; todo o resto continua igual:

TLS na 8883
var tlsOptions = new MqttClientOptionsBuilder()
    .WithTcpServer("broker.connectmq.com", 8883)
    .WithTlsOptions(o => o.UseTls())
    .WithCredentials(Environment.GetEnvironmentVariable("MQTT_USER"),
                     Environment.GetEnvironmentVariable("MQTT_PASS"))
    .Build();
WebSockets seguros na 8084
var wssOptions = new MqttClientOptionsBuilder()
    .WithWebSocketServer(o => o.WithUri("wss://broker.connectmq.com:8084/mqtt"))
    .WithTlsOptions(o => o.UseTls())
    .Build();

O certificado do broker é validado pelo repositório de confiança do sistema operacional. Resista à tentação de adicionar WithCertificateValidationHandler(_ => true) para sumir com um erro de handshake; procure a causa, começando por testar a conexão TLS pelo terminal. Versões antigas do MQTTnet 4 usavam .WithTls(), que agora está obsoleto.

Reconectar sem o ManagedMqttClient

O MQTTnet 4 oferecia o ManagedMqttClient, que reconectava e enfileirava mensagens para você. Ele saiu na versão 5, e os exemplos oficiais usam um loop simples no lugar. Aqui está uma versão completa:

Loop de reconexão
client.ConnectedAsync += async e =>
{
    // Com clean start o broker esqueceu nossas assinaturas; com sessão persistente talvez não.
    if (!e.ConnectResult.IsSessionPresent)
        await client.SubscribeAsync(subscribe, CancellationToken.None);
    Console.WriteLine("Conectado, sessão presente: " + e.ConnectResult.IsSessionPresent);
};

client.DisconnectedAsync += e =>
{
    Console.WriteLine($"Desconectado ({e.Reason}): {e.Exception?.Message}");
    return Task.CompletedTask;
};

using var cts = new CancellationTokenSource();
Console.CancelKeyPress += (_, a) => { a.Cancel = true; cts.Cancel(); };

var delay = TimeSpan.FromSeconds(1);
while (!cts.IsCancellationRequested)
{
    try
    {
        if (!await client.TryPingAsync(cts.Token))
        {
            await client.ConnectAsync(options, cts.Token);
            delay = TimeSpan.FromSeconds(1);           // zera o backoff após conectar
        }
    }
    catch (Exception ex) when (ex is not OperationCanceledException)
    {
        Console.WriteLine($"Falha ao reconectar: {ex.Message}");
        delay = TimeSpan.FromSeconds(Math.Min(delay.TotalSeconds * 2, 60));
    }

    try { await Task.Delay(delay + TimeSpan.FromMilliseconds(Random.Shared.Next(500)), cts.Token); }
    catch (OperationCanceledException) { break; }
}

await client.DisconnectAsync();

Três detalhes deixam isso robusto. A assinatura fica em ConnectedAsync, então é restaurada após cada reconexão, não só a primeira. A espera cresce exponencialmente com um pouco de jitter aleatório, para que mil clientes que perdem o broker ao mesmo tempo não reconectem todos no mesmo segundo — as boas práticas de reconexão explicam por que isso importa. E TryPingAsync funciona também como health check: retorna false tanto quando o cliente não está conectado quanto quando a conexão está meio aberta.

Buffer enquanto offline

O managed client também enfileirava mensagens de saída. Se você precisa disso, coloque um Channel<MqttApplicationMessage> entre seu código e o cliente: os produtores escrevem no canal e uma única task consumidora lê dele e chama PublishAsync apenas quando client.IsConnected é true, tentando de novo se a publicação lançar exceção. Limite o canal (Channel.CreateBounded) para que uma queda longa não esgote a memória, e decida explicitamente se descarta as mensagens mais antigas ou as mais novas quando ele encher.

Recursos do MQTT 5 que valem a pena

Com WithProtocolVersion(MqttProtocolVersion.V500) definido, algumas opções compensam na hora:

  • Session expiry — .WithSessionExpiryInterval(3600) junto com .WithCleanStart(false) mantém assinaturas e mensagens QoS 1 enfileiradas por uma hora após uma desconexão. Teste isso num broker privado; endpoints compartilhados de teste geralmente não guardam sessões offline.
  • Reason codes e reason strings — os resultados de connect, subscribe e publish trazem ResultCode/ReasonCode e muitas vezes um ReasonString. Registre-os em log; eles transformam "não funciona" em "cota excedida".
  • User properties — metadados chave/valor como um trace ID ou a versão do firmware, sem mudar o formato do payload.
  • Message expiry — .WithMessageExpiryInterval(60) numa mensagem impede que comandos vencidos sejam entregues a um dispositivo que reconecta uma hora depois.
  • Request/response — .WithResponseTopic() e .WithCorrelationData() oferecem um jeito padrão de implementar RPC sobre MQTT.

Teste

Rode o app console, abra o cliente MQTT online e assine testmqtt/dotnet/#. Você deve ver a leitura JSON chegar em testmqtt/dotnet/sensors/lab. Publique do navegador em testmqtt/dotnet/sensors/test e o handler C# imprime a mensagem. Para testar o loop de reconexão, desligue a rede por alguns segundos e veja o log passar de Desconectado para Conectado, sessão presente: False, seguido de um novo SUBACK.

Veja ao vivo as mensagens do seu cliente MQTTnet

Se você está construindo uma web API ou um worker service, o próximo passo é levar esse cliente para um hosted service com injeção de dependência e health checks — veja MQTT no ASP.NET Core.

O MQTTnet oferece uma API pequena e explícita: um builder de opções, três chamadas async e alguns eventos. Adicione um loop de reconexão que refaz as assinaturas em ConnectedAsync, registre todos os códigos de resultado, e o mesmo cliente funciona de um teste console no endpoint compartilhado até um serviço de produção no seu próprio broker.

Perguntas frequentes

O ManagedMqttClient existe no MQTTnet 5?

Não. O pacote MQTTnet.Extensions.ManagedClient não faz parte do MQTTnet 5. Use um IMqttClient comum com um pequeno loop de reconexão (TryPingAsync mais ConnectAsync), refaça as assinaturas no handler ConnectedAsync e, se precisar, guarde as mensagens de saída num Channel.

O que mudou entre o MQTTnet 4 e o 5?

As principais mudanças incompatíveis: MqttFactory virou MqttClientFactory, os tipos de cliente saíram de MQTTnet.Client para o namespace MQTTnet, a extensão do managed client foi removida e o payload passou a ser um ReadOnlySequence<byte>. A maioria dos métodos dos builders manteve o nome.

Como usar MQTT 5 com o MQTTnet?

Chame WithProtocolVersion(MqttProtocolVersion.V500) no builder de opções do cliente. Você passa a ter reason codes nos resultados de connect, subscribe e publish, além de session expiry, user properties, message expiry e propriedades de request/response.