MAUI 开发安卓 MQTT 客户端:实现远程控制 (完整源码 + 避坑指南)

在物联网开发场景中,MQTT 协议凭借轻量、低功耗的特性成为设备通信的首选。本文基于.NET MAUI 框架,开发一款可在安卓平台运行的 MQTT 客户端,实现与 MQTT 服务器的连接、消息发布 / 订阅,并通过指令远程控制 LED 设备。相比传统安卓原生开发,MAUI 仅需一套代码即可适配安卓 /iOS 等平台,大幅降低开发成本。

一、开发环境准备

1. 基础环境

  • 开发工具:Visual Studio 2022(需安装 ".NET MAUI" 工作负载)
  • 运行框架:.NET 7/8(推荐.NET 8,兼容性更好)
  • 依赖库:MQTTnet(NuGet 安装,版本≥4.0.0)
  • 测试环境:安卓模拟器(API 33+)或安卓真机(Android 8.0+)

2. NuGet 包安装

在项目中安装 MQTTnet 核心包:

cs 复制代码
Install-Package MQTTnet -Version 4.3.7.789

二、核心功能设计

本次实现的安卓 MQTT 客户端包含以下核心功能:

  1. 基于 Grid 布局的简洁 UI(连接按钮、开灯 / 关灯按钮、日志显示区);
  2. MQTT 服务器 TLS 加密连接(适配 EMQ X 公共服务器);
  3. 订阅 / 发布 MQTT 主题,实现 LED 控制指令传输;
  4. 带时间戳的日志系统(限制日志条数、自动滚动到底部);
  5. 页面生命周期管理(退出时自动断开 MQTT 连接)。

三、完整代码实现

1. XAML 布局(MainPage.xaml)

采用 Grid 嵌套布局适配安卓屏幕,解决 MAUI 布局兼容性问题,按钮状态默认禁用(连接后启用):

cs 复制代码
<?xml version="1.0" encoding="utf-8"?>
<ContentPage xmlns="http://schemas.microsoft.com/dotnet/2021/maui"
             xmlns:x="http://schemas.microsoft.com/winfx/2009/xaml"
             x:Class="MauiMqttClient.MainPage"
             Title="MQTT LED控制">

    <!-- 外层Grid:分两行(按钮区+日志区) -->
    <Grid Padding="20"
          RowDefinitions="Auto, *"
          RowSpacing="15">

        <!-- 第一行:功能按钮区(三列均分) -->
        <Grid Grid.Row="0"
              ColumnDefinitions="*,*,*"
              ColumnSpacing="10"
              HorizontalOptions="Fill">

            <Button x:Name="btnConnect"
                    Text="连接服务器"
                    Clicked="BtnConnect_Clicked"
                    BackgroundColor="#2196F3"
                    TextColor="White"
                    Grid.Column="0"/>

            <Button x:Name="btnLedOn"
                    Text="开灯"
                    Clicked="BtnLedOn_Clicked"
                    BackgroundColor="#4CAF50"
                    TextColor="White"
                    Grid.Column="1"
                    IsEnabled="False"/>

            <Button x:Name="btnLedOff"
                    Text="关灯"
                    Clicked="BtnLedOff_Clicked"
                    BackgroundColor="#F44336"
                    TextColor="White"
                    Grid.Column="2"
                    IsEnabled="False"/>

        </Grid>

        <!-- 第二行:日志显示区(只读Editor) -->
        <Editor x:Name="editorLog"
                Grid.Row="1"
                IsReadOnly="True"
                BackgroundColor="White"
                TextColor="Black"
                FontSize="18"
                AutoSize="Disabled"/>

    </Grid>
</ContentPage>

2. 后台逻辑代码(MainPage.xaml.cs)

核心包含 MQTT 客户端初始化、事件绑定、消息收发、日志优化等,重点适配安卓端 TLS 连接和 UI 线程限制:

cs 复制代码
using System;
using System.Text;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
using System.Linq;
using System.Security.Authentication;
using Microsoft.Maui.Controls;
using MQTTnet;
using MQTTnet.Client;
using MQTTnet.Protocol;
using MQTTnet.Packets;

namespace MauiMqttClient
{
    public partial class MainPage : ContentPage
    {
        private IMqttClient? _mqttClient;
        private bool _isConnected = false;
        private readonly string _clientId = Guid.NewGuid().ToString();

        // MQTT服务器配置(EMQ X公共TLS服务器)
        private const string Broker = "p6121ba8.ala.cn-hangzhou.emqxsl.cn"; 
        private const int Port = 8883;
        private const string Username = ""; // 无则留空
        private const string Password = ""; // 无则留空
        private const string PublishTopic = "/pctostm32/test"; // 发布指令主题
        private const string SubscribeTopic = "/stm32topc/test"; // 订阅设备反馈主题

        // 日志缓存(限制最大条数,避免安卓内存溢出)
        private readonly List<string> _logEntries = new List<string>();
        private const int MaxLogLines = 1000;

        public MainPage()
        {
            InitializeComponent();

            // 安卓端TLS连接关键配置:忽略证书验证+指定TLS12
            ServicePointManager.ServerCertificateValidationCallback = (sender, cert, chain, errors) => true;
            ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls12;

            InitMqttClient();
            AddLog("初始化完成,点击「连接服务器」开始操作");
        }

        #region MQTT客户端初始化与事件绑定
        /// <summary>
        /// 初始化MQTT客户端实例
        /// </summary>
        private void InitMqttClient()
        {
            try
            {
                var factory = new MqttFactory();
                _mqttClient = factory.CreateMqttClient();
                BindMqttEvents(); // 绑定MQTT核心事件
            }
            catch (Exception ex)
            {
                AddLog($"⚠️ 客户端初始化失败:{ex.Message}");
                _mqttClient = null;
            }
        }

        /// <summary>
        /// 绑定MQTT连接/断开/消息接收事件(避免重复绑定)
        /// </summary>
        private void BindMqttEvents()
        {
            if (_mqttClient == null) return;

            // 先解绑再绑定,防止重复订阅事件
            _mqttClient.ConnectedAsync -= OnMqttConnected;
            _mqttClient.ConnectedAsync += OnMqttConnected;

            _mqttClient.DisconnectedAsync -= OnMqttDisconnected;
            _mqttClient.DisconnectedAsync += OnMqttDisconnected;

            _mqttClient.ApplicationMessageReceivedAsync -= OnMqttMessageReceived;
            _mqttClient.ApplicationMessageReceivedAsync += OnMqttMessageReceived;
        }
        #endregion

        #region MQTT事件处理(需切换到安卓UI线程)
        /// <summary>
        /// MQTT连接成功事件
        /// </summary>
        private async Task OnMqttConnected(MqttClientConnectedEventArgs e)
        {
            _isConnected = true;
            // 安卓UI操作必须在MainThread执行
            await MainThread.InvokeOnMainThreadAsync(() =>
            {
                btnConnect.Text = "断开服务器";
                btnLedOn.IsEnabled = true;
                btnLedOff.IsEnabled = true;
                AddLog("✅ 连接服务器成功");
            });
            await SubscribeTopicAsync(); // 连接成功后订阅主题
        }

        /// <summary>
        /// MQTT断开连接事件
        /// </summary>
        private async Task OnMqttDisconnected(MqttClientDisconnectedEventArgs e)
        {
            _isConnected = false;
            await MainThread.InvokeOnMainThreadAsync(() =>
            {
                btnConnect.Text = "连接服务器";
                btnLedOn.IsEnabled = false;
                btnLedOff.IsEnabled = false;
                var reason = e.Reason + (e.Exception != null ? $" | 异常:{e.Exception.Message}" : "");
                AddLog($"❌ 断开连接:{reason}");
            });
        }

        /// <summary>
        /// 接收MQTT消息事件
        /// </summary>
        private async Task OnMqttMessageReceived(MqttApplicationMessageReceivedEventArgs e)
        {
            string topic = e.ApplicationMessage.Topic;
            byte[] payloadBytes = e.ApplicationMessage.PayloadSegment.ToArray();
            string payload = Encoding.UTF8.GetString(payloadBytes);

            await MainThread.InvokeOnMainThreadAsync(() =>
            {
                AddLog($"📩 收到消息:{topic} → {payload}");
            });
        }
        #endregion

        #region 按钮点击事件
        /// <summary>
        /// 连接/断开服务器按钮
        /// </summary>
        private async void BtnConnect_Clicked(object sender, EventArgs e)
        {
            if (_mqttClient == null)
            {
                AddLog("❌ MQTT客户端未初始化");
                return;
            }

            if (!_isConnected)
            {
                // 构建MQTT连接选项(适配TLS加密)
                var mqttOptions = new MqttClientOptionsBuilder()
                    .WithTcpServer(Broker, Port)
                    .WithClientId(_clientId)
                    .WithCredentials(Username, Password)
                    .WithTls(options =>
                    {
                        options.UseTls = true;
                        options.SslProtocol = SslProtocols.Tls12;
                        options.AllowUntrustedCertificates = true;
                        options.IgnoreCertificateChainErrors = true;
                        options.IgnoreCertificateRevocationErrors = true;
                        options.CertificateValidationHandler = (context) => true;
                    })
                    .WithCleanSession()
                    .WithKeepAlivePeriod(TimeSpan.FromSeconds(30))
                    .WithoutThrowOnNonSuccessfulConnectResponse()
                    .Build();

                try
                {
                    await _mqttClient.ConnectAsync(mqttOptions, CancellationToken.None);
                }
                catch (Exception ex)
                {
                    var errorMsg = ex.ToString() + (ex.InnerException != null ? $"\n内部异常:{ex.InnerException.Message}" : "");
                    AddLog($"❌ 连接失败:{errorMsg}");
                    _isConnected = false;
                }
            }
            else
            {
                // 断开连接并重置客户端
                if (_mqttClient.IsConnected)
                {
                    await UnsubscribeTopicAsync();
                    await _mqttClient.DisconnectAsync();
                }

                _mqttClient = null;
                _isConnected = false;
                btnConnect.Text = "连接服务器";
                btnLedOn.IsEnabled = false;
                btnLedOff.IsEnabled = false;
                AddLog("已主动断开与服务器的连接");
                InitMqttClient(); // 重新初始化客户端
            }
        }

        /// <summary>
        /// 开灯按钮:发布led on指令
        /// </summary>
        private async void BtnLedOn_Clicked(object sender, EventArgs e)
        {
            await PublishMessageAsync("led on");
        }

        /// <summary>
        /// 关灯按钮:发布led off指令
        /// </summary>
        private async void BtnLedOff_Clicked(object sender, EventArgs e)
        {
            await PublishMessageAsync("led off");
        }
        #endregion

        #region MQTT订阅/发布核心方法
        /// <summary>
        /// 订阅指定MQTT主题
        /// </summary>
        private async Task SubscribeTopicAsync()
        {
            if (_mqttClient == null || !_mqttClient.IsConnected) return;

            try
            {
                var topicFilter = new MqttTopicFilter
                {
                    Topic = SubscribeTopic,
                    QualityOfServiceLevel = MqttQualityOfServiceLevel.AtMostOnce
                };

                var subscribeOptions = new MqttClientSubscribeOptions
                {
                    TopicFilters = new List<MqttTopicFilter> { topicFilter }
                };

                var subscribeResult = await _mqttClient.SubscribeAsync(subscribeOptions, CancellationToken.None);

                bool isSuccess = subscribeResult.Items.Any() && (int)subscribeResult.Items.First().ResultCode == 0;
                AddLog(isSuccess ? $"✅ 订阅主题成功:{SubscribeTopic}" : $"❌ 订阅失败:{subscribeResult.Items.First().ResultCode}");
            }
            catch (Exception ex)
            {
                AddLog($"❌ 订阅异常:{ex.Message}");
            }
        }

        /// <summary>
        /// 取消订阅指定MQTT主题
        /// </summary>
        private async Task UnsubscribeTopicAsync()
        {
            if (_mqttClient == null || !_mqttClient.IsConnected) return;

            try
            {
                var unsubscribeOptions = new MqttClientUnsubscribeOptions
                {
                    TopicFilters = new List<string> { SubscribeTopic }
                };

                await _mqttClient.UnsubscribeAsync(unsubscribeOptions, CancellationToken.None);
                AddLog($"已取消订阅主题:{SubscribeTopic}");
            }
            catch (Exception ex)
            {
                AddLog($"❌ 取消订阅异常:{ex.Message}");
            }
        }

        /// <summary>
        /// 发布MQTT消息到指定主题
        /// </summary>
        private async Task PublishMessageAsync(string message)
        {
            if (_mqttClient == null || !_mqttClient.IsConnected)
            {
                AddLog("❌ 未连接服务器,无法发布消息");
                return;
            }

            try
            {
                var mqttMessage = new MqttApplicationMessageBuilder()
                    .WithTopic(PublishTopic)
                    .WithPayload(Encoding.UTF8.GetBytes(message))
                    .WithQualityOfServiceLevel(MqttQualityOfServiceLevel.AtMostOnce)
                    .WithRetainFlag(false)
                    .Build();

                await _mqttClient.PublishAsync(mqttMessage, CancellationToken.None);
                AddLog($"📤 发布消息:{PublishTopic} → {message}");
            }
            catch (Exception ex)
            {
                AddLog($"❌ 发布失败:{ex.Message}");
            }
        }
        #endregion

        #region 日志辅助方法(适配安卓UI滚动)
        /// <summary>
        /// 添加日志并自动滚动到底部(安卓端关键优化)
        /// </summary>
        private void AddLog(string content)
        {
            _ = MainThread.InvokeOnMainThreadAsync(() =>
            {
                // 1. 生成带毫秒级时间戳的日志
                string timeStamp = DateTime.Now.ToString("HH:mm:ss:fff");
                string logEntry = $"[{timeStamp}] {content}";

                // 2. 限制日志条数,防止安卓内存溢出
                _logEntries.Add(logEntry);
                if (_logEntries.Count > MaxLogLines)
                {
                    _logEntries.RemoveAt(0); // 移除最旧日志
                }

                // 3. 拼接日志并赋值
                editorLog.Text = string.Join(Environment.NewLine, _logEntries);

                // 4. 强制滚动到日志末尾(安卓端Editor滚动失效修复)
                editorLog.CursorPosition = editorLog.Text.Length;
            });
        }
        #endregion

        #region 页面生命周期管理
        /// <summary>
        /// 页面消失时断开MQTT连接,释放资源(安卓端防止内存泄漏)
        /// </summary>
        protected override void OnDisappearing()
        {
            base.OnDisappearing();
            if (_mqttClient != null && _mqttClient.IsConnected)
            {
                _ = _mqttClient.DisconnectAsync();
            }
            _mqttClient = null;
        }
        #endregion
    }
}

四、安卓端测试与运行

1. 调试配置

  1. 打开 VS2022,将项目调试目标切换为安卓模拟器(如 Pixel 5 - API 33)或已开启 "开发者模式 + USB 调试" 的安卓真机;
  2. 确认 MQTT 服务器地址(Broker)和端口(Port)正确,若使用私有 MQTT 服务器,需替换为对应地址。

2. 运行效果

  1. 启动应用后,日志区显示 "初始化完成";
  2. 点击「连接服务器」,成功后按钮变为 "断开服务器",开灯 / 关灯按钮启用;
  3. 点击「开灯」/「关灯」,日志区显示发布的指令;
  4. 若设备端(如 STM32)向/stm32topc/test主题发送反馈消息,应用会实时接收并显示。

五、安卓端常见问题与避坑指南

1. TLS 连接失败

  • 问题:安卓端连接 TLS 加密的 MQTT 服务器时提示 "证书验证失败";
  • 解决:必须设置ServicePointManager忽略证书验证,并指定SslProtocols.Tls12(安卓默认不兼容高版本 TLS)。

2. Editor 日志滚动失效

  • 问题:日志增多后,Editor 无法自动滚动到底部;
  • 解决:通过MainThread切换到 UI 线程,设置CursorPosition = editorLog.Text.Length(MAUI 安卓端ScrollToEnd方法偶发失效)。

3. MQTT 事件不触发

  • 问题:连接成功后收不到消息或不触发 Connected 事件;
  • 解决:事件绑定前先解绑(-=)再绑定(+=),避免重复订阅导致事件失效。

4. 内存泄漏

  • 问题:多次进入 / 退出页面后,安卓端内存占用过高;
  • 解决:在OnDisappearing方法中断开 MQTT 连接并释放_mqttClient实例,同时限制日志最大条数。

六、扩展方向

  1. 断线重连:添加 MQTT 自动重连逻辑,适配安卓网络切换场景;
  2. 多设备控制:扩展主题配置,支持同时控制多个 LED 设备;
  3. UI 美化:添加状态图标、主题切换,适配安卓深色模式;
  4. 权限适配:若使用非标准端口,添加安卓网络权限配置。

总结

本文基于.NET MAUI 实现了安卓平台的 MQTT LED 控制客户端,核心解决了安卓端 TLS 连接、UI 线程切换、日志滚动等关键问题。相比安卓原生开发,MAUI 实现了 "一套代码多端运行",大幅提升开发效率。代码已适配安卓 8.0 + 版本,可直接移植到实际物联网项目中,仅需替换 MQTT 服务器配置即可快速上线。

服务器端可参考我之前的文章基于 RT-Thread Studio 实战:ESP8266+MQTT-CSDN博客

仓库地址:csl/MauiMqttClient

相关推荐
成都大菠萝2 小时前
2-2-44 快速掌握Kotlin-函数类型操作
android
WebRuntime2 小时前
问世间,exe是何物?直教AI沉默、Web寡言(4)
javascript·c#·.net·web
有位神秘人3 小时前
Android中获取设备里面的音频文件
android
缺点内向3 小时前
如何在 C# 中将 Word 文档转换为 EMF(增强型图元文件)
开发语言·c#·word·.net
2501_915918413 小时前
使用 HBuilder 上架 iOS 应用时常见的问题与应对方式
android·ios·小程序·https·uni-app·iphone·webview
MyBFuture4 小时前
C# 哈希表与堆栈队列实战指南
开发语言·windows·c#·visual studio
farewell-Calm4 小时前
01_Android快速入门
android
helloCat4 小时前
记录CI/CD自动化上传AppGallery遇到的坑
android·前端·api