12+ Jahre App-Entwicklung
Alles aus einer Hand
50+ Erfolgreiche App-Projekte

Blog

Bidirektionale Kommunikation mit MQTT in .NET MAUI

Als Mobile-App-Entwickler:innen müssen wir ständig Informationen zwischen App und Backend austauschen. In den meisten Fällen ist eine RESTful-API die Lösung. Aber was, wenn ein konstanter Datenfluss in beide Richtungen benötigt wird? In diesem Beitrag schauen wir uns MQTT an und wie man eine einfache Chat-App in .NET MAUI erstellt.

Artikelbild für Bidirektionale Kommunikation mit MQTT in .NET MAUI

Inhaltsverzeichnis

  1. Wie funktioniert MQTT?
  2. Einen einfachen MQTT-Server mit MQTTnet erstellen
  3. Eine einfache .NET-MAUI-Chat-App
  4. Ergebnis testen
  5. Herausforderungen

MQTT steht für Message Queue Telemetry Transport und ist ein Machine-to-Machine-(M2M)-Protokoll. Es dient dem Austausch von Nachrichten zwischen Sensoren, Smartphones, Autos oder allem, was dir sonst noch einfällt. Dafür braucht man einen Subscriber und einen Publisher, die beide mit einem Server – dem sogenannten Broker – verbunden sind.

Wie funktioniert MQTT?

MQTT nutzt ein Publish/Subscribe-Modell, um Nachrichten an einen oder mehrere Clients zu senden. Clients haben keine Adressen wie in E-Mail-Systemen, und Nachrichten werden ihnen nicht direkt zugestellt. Stattdessen veröffentlicht ein Publisher Nachrichten auf einem Topic, und ein Subscriber muss dieses Topic abonnieren, um die Nachricht zu erhalten – wie beim TV- oder Radio-Prinzip.

Die Aufgabe eines MQTT-Brokers ist es, Nachrichten anhand ihrer Topics zu filtern und sie anschließend an die entsprechenden Subscriber zu verteilen. Ein Client kann diese Nachrichten empfangen, indem er auf demselben Broker das Topic abonniert. Es gibt keine direkte Verbindung zwischen Publisher und Subscriber. Alle Clients können veröffentlichen („broadcasten“) und abonnieren („empfangen“).

MQTT overview

Was ist ein MQTT-Broker?

Der Broker ist dafür verantwortlich, alle Nachrichten zu empfangen, zu filtern, zu bestimmen, wer welches Topic abonniert hat, und die Nachricht an diese Clients zu senden. Der Broker ist das Herzstück jedes Publish/Subscribe-Protokolls. Je nach Implementierung kann ein Broker Millionen gleichzeitig verbundener MQTT-Clients verwalten.

Was ist ein MQTT-Client?

Ein MQTT-Client ist jedes Gerät (vom Microcontroller bis zum vollwertigen Server), das eine MQTT-Bibliothek ausführt und sich über ein Netzwerk mit einem MQTT-Broker verbindet.

Clientname bzw. Client-ID

Alle Clients müssen einen Clientnamen oder eine Client-ID haben, um am Nachrichtenaustausch teilzunehmen. Der Broker nutzt diese, um Subscriptions zu verfolgen; sie muss daher eindeutig sein. Versuchst du, dich mit demselben Namen wie ein bereits verbundener Client zu verbinden, wird dessen Verbindung getrennt. Da die meisten Implementierungen nach einer Trennung automatisch reconnecten, kann das zu einer Endlosschleife aus Disconnect und Connect führen.

Clean Sessions

Standardmäßig stellen MQTT-Clients eine Clean Session mit einem Broker her. Das bedeutet, der Broker soll sich nichts über den Client merken, wenn er sich trennt. In einer unclean session merkt sich der Broker Client-Subscriptions und kann unzugestellte Nachrichten für den Client speichern. Das hängt allerdings von der Quality of Service (QoS) beim Abonnieren/Publishen der Topics ab.

Einen einfachen MQTT-Server mit MQTTnet erstellen

Für unsere .NET-MAUI-Chat-App benötigen wir einen Broker. Dazu erstellen wir ein neues Console-Projekt und installieren die MQTTnet-Pakete. Mit folgendem Code erzeugen wir den einfachsten MQTT-Server mit einem TCP-Endpoint (Standard: Port 1883) und stoppen ihn per Tastendruck:

var mqttServer = new MqttFactory().CreateMqttServer();
mqttServer.StartAsync(new MqttServerOptions());

Console.WriteLine("Press any key to exit.");
Console.ReadLine();

mqttServer.StopAsync();

Den MQTT-Server mit MqttServerOptionsBuilder konfigurieren

Mit dem MqttServerOptionsBuilder kannst du den Broker anpassen, z. B. den Port auf 1884 ändern oder eine Server-Client-ID setzen. Außerdem kannst du alle empfangenen Nachrichten in einer Logdatei speichern – etwa in MessagesLog.txt.

const string LogFilename = "/Users/{user}/Desktop/MessagesLog.txt";

var option = new MqttServerOptionsBuilder()
    .WithDefaultEndpoint()
    .WithDefaultEndpointPort(1884);

var mqttServer = new MqttFactory().CreateMqttServer(option.Build());
mqttServer.InterceptingPublishAsync += context =>
{
    var message = Encoding.UTF8.GetString(context.ApplicationMessage.Payload);

    if (File.Exists(LogFilename) == false)
        File.CreateText(LogFilename);

    File.AppendAllText(LogFilename,
        $"Client: {context.ClientId}, sent time: {DateTime.Now}, message: {JsonConvert.SerializeObject(message)} \r\n");

    return CompletedTask.Instance;
};

Nachrichten für inaktive Clients aufbewahren

Ist ein Client getrennt, kann der Broker empfangene Nachrichten speichern und sie beim erneuten Verbinden versenden. Diese Nachrichten nennt man Retained Messages. Mehr zum Retained-Flag gibt’s in der MQTT Essentials*-Reihe von HiveMQ. MQTTnet bietet Events zum Speichern und Laden solcher Nachrichten. Füge die RetainedMessage-Events zu deinem mqttServer hinzu. Das Senden übernimmt der Server automatisch. Den WithApplicationMessageInterceptor kannst du weglassen, wenn du ihn nicht brauchst.

mqttServer.LoadingRetainedMessageAsync += async eventArgs =>
{
    try
    {
        var json = await File.ReadAllTextAsync(RetainedMessagesFilename);
        eventArgs.LoadedRetainedMessages = JsonConvert
            .DeserializeObject<List<MqttApplicationMessage>>(json);
    }
    catch (FileNotFoundException)
    {
        Console.WriteLine("No retained messages stored yet.");
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception);
    }
};

mqttServer.RetainedMessageChangedAsync += async eventArgs =>
{
    try
    {
        if (File.Exists(RetainedMessagesFilename) == false)
            File.CreateText(RetainedMessagesFilename);

        File.WriteAllText(RetainedMessagesFilename,
            JsonConvert.SerializeObject(eventArgs.StoredRetainedMessages));
    }
    catch (Exception exception)
    {
        Console.WriteLine(exception);
    }
};

Falls du deine Logs nicht findest, suche die Datei mit dem Finder (macOS) oder dem Windows Explorer – sie sollte vorhanden sein.

Das ist alles, was wir für den Broker brauchen.

Du hast ein Projekt in Xamarin oder .NET MAUI? Wir helfen dir, Zeit zu sparen.

Bevor du alleine weitermachst, wirf einen Blick auf unsere Services. Unser erfahrenes Team bespricht dein Projekt gern mit dir und hilft dir dabei, es auf exzellentem Niveau fertigzustellen.

Unsere App-Entwicklungsleistungen

Eine einfache .NET-MAUI-Chat-App

Mit dem Broker am Start erstellen wir nun eine .NET-MAUI-App, die MQTTnet nutzt, um Nachrichten zwischen Benutzer:innen nahezu in Echtzeit auszutauschen.

Zuerst legen wir ein neues Projekt an und richten den MQTT-Client in einer ViewModel-Klasse ein. Für WithTcpServer verwendest du deine IP-Adresse, und als Client-ID den aktuellen User.

IMqttClient _mqttClient;
string _currentUser = "user1";
string _chatPartner = "user2";

public async Task CreateClient()
{
    // Clientobjekt erzeugen
    _mqttClient = new MqttFactory().CreateMqttClient();

    var options = new MqttClientOptionsBuilder()
                    .WithClientId(_currentUser)
                    .WithTcpServer("YOUR IP", 1884)
                    .Build();
}

Wir hängen außerdem Events in CreateClient an, um benachrichtigt zu werden, wenn der Client verbunden/getrennt ist oder Nachrichten empfängt.

// Events registrieren
._mqttClient.ConnectedAsync += MqttClient_ConnectedAsync;
_mqttClient.DisconnectedAsync += MqttClient_DisconnectedAsync;
_mqttClient.ApplicationMessageReceivedAsync += MqttClient_ApplicationMessageReceivedAsync;

Bei Connected und Disconnected loggen wir den Status. Bei eingehenden Nachrichten aktualisieren wir den Chatverlauf mit Nachricht und Sender:

private Task MqttClient_ApplicationMessageReceivedAsync(MqttApplicationMessageReceivedEventArgs arg)
{
    ChatHistory += $"{arg.ClientId}: \n{Encoding.UTF8.GetString(arg.ApplicationMessage.Payload)}\n";
    return Task.CompletedTask;
}

private Task MqttClient_DisconnectedAsync(MqttClientDisconnectedEventArgs arg)
{
    Console.WriteLine("DISCONNECTED");
    return Task.CompletedTask;
}

private Task MqttClient_ConnectedAsync(MqttClientConnectedEventArgs arg)
{
    Console.WriteLine("CONNECTED");
    return Task.CompletedTask;
}

Zuletzt verbinden wir uns mit dem Broker und abonnieren unseren Chat-Kanal, um über neue Nachrichten informiert zu werden.

// Mit dem Broker verbinden
await _mqttClient.ConnectAsync(options);
await _mqttClient.SubscribeAsync(
    new MqttClientSubscribeOptionsBuilder()
        .WithTopicFilter($"chatChannel/{_currentUser}")
        .Build());

Jetzt bereiten wir die Seite vor, um empfangene Nachrichten anzuzeigen und eigene Nachrichten zu senden. Zum Senden erstellen wir eine MqttApplicationMessage, die den Empfänger-Kanal und den Text enthält. Mit PublishAsync wird sie verschickt.

Ich verwende den MVVM Source Generator, um dieses ViewModel zu implementieren.

public partial class MainViewModel : ObservableObject
{
    [ObservableProperty]
    string _chatHistory;

    [ObservableProperty]
    string _message;

    [RelayCommand]
    async void Send()
    {
        var applicationMessage = new MqttApplicationMessageBuilder()
                                    .WithTopic($"chatChannel/{_chatPartner}")
                                    .WithPayload(Message)
                                    .Build();

        await _mqttClient.PublishAsync(applicationMessage);

        ChatHistory += $"You: \n{Message}\n";
        Message = string.Empty;
    }
}

Für das View benötigen wir einen Editor zur Anzeige aller Nachrichten, ein Entry zum Eingeben und einen Button zum Senden.

<Grid RowDefinitions="9*,1*" ColumnDefinitions="8*,2*">
    <Editor Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" IsReadOnly="True" Text="{Binding ChatHistory}"/>
    <Entry Grid.Row="1" Grid.Column="0" Placeholder="Enter message" Text="{Binding Message}"/>
    <Button Grid.Row="1" Grid.Column="1" Text="Send" Command="{Binding SendCommand}"/>
</Grid>

Vergiss nicht, das Binding zwischen ViewModel und Page herzustellen. Erzeuge außerdem den Client in OnAppearing() über CreateClient().

MainViewModel _viewModel;

public MainPage(MainViewModel viewModel)
{
    InitializeComponent();

    BindingContext = _viewModel = viewModel;
}

protected override void OnAppearing()
{
    base.OnAppearing();

    _viewModel.CreateClient().ContinueWith(t => { });
}

Den Code findest du in unserem Broker-Repository und Client-Repository.

Ergebnis testen

Zum Schluss startest du den Broker und zwei Simulatoren. Starte den zweiten Simulator mit vertauschten Variablen _currentUser und _chatPartner:

string _currentUser = "user2";
string _chatPartner = "user1";

Tippe einfach deine Nachricht ein und sende sie. Beide Simulatoren sollten die gesendete Nachricht sehen. Du kannst auch beliebig viele Simulatoren starten und dasselbe Topic abonnieren. Wenn ein Client eine Nachricht an dieses Topic veröffentlicht, erhalten alle Subscriber die Nachricht. Denk daran, für jeden Client eine eigene Client-ID zu verwenden.

Gefällt dir unser Ansatz?

Du hast es bis hierher geschafft – als Entwickler:in hast du einen Einblick in unsere Arbeit erhalten. Migrationen sind nur ein Teil unserer Xamarin- und .NET-MAUI-Leistungen. Wir unterstützen dich in allen Bereichen der App-Entwicklung.

Lass uns sprechen

Herausforderungen

Wenn du die App in den Hintergrund schickst, erhältst du weiterhin Nachrichten, solange deine App lebt – du wirst also nicht vom Server getrennt. Mit Local Notifications kannst du Nutzer:innen über neue Nachrichten informieren.

Aber was passiert, wenn deine App vom OS beendet wurde?

In diesem Fall weißt du nicht, ob Nachrichten eingegangen sind, da du getrennt bist. Das lässt sich mit Push-Benachrichtigungen lösen: Sie hängen nicht von deiner Broker-Verbindung ab. Sobald der/die Nutzer:in die Benachrichtigung sieht, kann er/sie die App starten, erneut verbinden und die neuesten Nachrichten abrufen.

Lass mich wissen, ob dir dieser Artikel geholfen hat. Bei Fragen melde dich gern.

Martin Luong

Martin Luong

Mit über 7 Jahren Erfahrung in der Entwicklung von Cross-Plattform-Apps mit Xamarin und .NET MAUI zählt Martin zu den alten Hasen bei Cayas Software. Kunden aus Agrar-, Logistik- oder der Gesundheitsbranche schätzen seine ruhige Art und das analytische Vorgehen bei der Umsetzung ihrer Xamarin und .NET MAUI-Projekte. Ein Teil aus seiner täglichen Arbeit mit Xamarin und .NET MAUI teilt er in seinen Artikeln.

Verwandte Artikel

Erstellen eines .NET MAUI Karten-Steuerelements
Erstellen eines .NET MAUI Karten-Steuerelements

Ich arbeite derzeit an der Portierung einer Xamarin Forms App zu .NET MAUI. Die App verwendet auch Karten von Apple oder Google Maps, um Standorte anzuzeigen. Obwohl es bis zur Veröffentlichung von .NET 7 keine offizielle Unterstützung in MAUI gab, möchte ich Ihnen eine Möglichkeit zeigen, Karten über einen benutzerdefinierten Handler anzuzeigen.

Responsive Layouts in .NET MAUI
Responsive Layouts in .NET MAUI

.NET MAUI ermöglicht es uns, plattform- und geräteunabhängige Anwendungen zu schreiben, was eine dynamische Anpassung an die Bildschirmgröße und -form des Benutzers erforderlich macht. In diesem Blog-Beitrag erfahren Sie, wie Sie Ihre XAML-Layouts an unterschiedliche Geräteausrichtungen anpassen können. Dabei verwenden Sie eine ähnliche Syntax wie OnIdiom und OnPlatform, die Ihnen vielleicht schon bekannt ist.

Using voice commands in .NET MAUI
Using voice commands in .NET MAUI

This post is a continuation of the Hackathon topic post, where the technical implementation of voice commands in .NET MAUI is revealed, as well as the challenges the development team faced and how they successfully solved them.