C# WinForms 实战:MQTTS 客户端开发(与 STM32 设备通信)

在物联网开发中,MQTT 是设备与上位机通信的常用协议,而 **MQTTS(加密 MQTT)** 能保障数据传输的安全性。本文基于 C# WinForms + uPLibrary.M2Mqtt 库,实现一个可与 STM32 等设备通信的 MQTTS 客户端,涵盖连接、订阅、发布、设备控制、日志管理等核心功能。

一、项目概述

本项目实现的 MQTTS 客户端具备以下功能:

  • 连接 / 断开加密 MQTT 服务器(MQTTS,端口 8883);
  • 订阅设备主题、接收设备消息;
  • 发布消息到设备(含 LED 开关等控制指令);
  • 管理订阅主题列表;
  • 日志记录与导出;
  • 适配 WinForms 跨线程 UI 更新。

二、开发准备

2.1 环境配置

  • 开发工具:Visual Studio 2019+;
  • 框架版本:.NET Framework 4.7.2(兼容多数 Windows 环境);
  • 协议:MQTTS(基于 TLS 1.2 加密)。

2.2 依赖库安装

通过 NuGet 安装 MQTT 客户端库:

  1. 右键项目 → 选择「管理 NuGet 程序包」;
  2. 搜索「M2Mqtt」,安装uPLibrary.Networking.M2Mqtt(本文使用 4.3.0 版本)。

三、界面快速设计

WinForms 界面包含以下核心控件:

  • 输入控件:服务器地址 / 端口、订阅主题、发布主题、消息、用户名 / 密码;
  • 功能按钮:连接 / 断开、订阅、发布、LED 开关、清空日志、导出日志;
  • 列表控件ListView(显示已订阅主题);
  • 日志控件RichTextBox(记录操作与消息日志)。

四、核心功能代码解析

4.1 MQTT 客户端初始化

在窗体构造函数中完成客户端、UI 状态、SSL 配置的初始化:

public partial class Form1 : Form
{
    private MqttClient _mqttClient; // MQTT客户端对象
    private bool _isConnected;      // 连接状态
    private string _clientId;       // 唯一客户端ID(用GUID避免重复)
    // 存储已订阅主题(主题→QoS)
    private readonly Dictionary<string, byte> _subscribedTopics = new Dictionary<string, byte>();

    public Form1()
    {
        InitializeComponent();
        _clientId = Guid.NewGuid().ToString(); // 生成唯一客户端ID
        _isConnected = false;

        // 初始化UI:未连接时功能按钮置灰
        btnSubscribe.Enabled = btnPublish.Enabled = false;

        // 默认配置(替换为你的MQTTS服务器信息)
        leHostPort.Text = "p6121ba8.ala.cn-hangzhou.emqxsl.cn:8883";
        leSubTopic.Text = "/stm32topc/test"; // 设备→PC主题
        lePubTopic.Text = "/pctostm32/test"; // PC→设备主题

        // 测试环境跳过SSL证书验证(生产环境需启用验证)
        ServicePointManager.ServerCertificateValidationCallback += (s, cert, chain, err) => true;
        // 启用TLS 1.2(MQTTS必需)
        ServicePointManager.SecurityProtocol = SecurityProtocolType.Tls11 | SecurityProtocolType.Tls12;
    }
}

4.2 连接 / 断开 MQTTS 服务器

通过btnConnect_Click实现连接逻辑,包含地址校验、SSL 客户端初始化、连接状态更新:

private void btnConnect_Click(object sender, EventArgs e)
{
    if (!_isConnected)
    {
        // 校验地址格式([地址]:[端口])
        var parts = leHostPort.Text.Split(':');
        if (parts.Length != 2 || !int.TryParse(parts[1], out int port))
        {
            AddLog("地址格式错误(例:broker.emqx.io:8883)", true);
            return;
        }
        string broker = parts[0];

        try
        {
            // 初始化MQTTS客户端(启用SSL)
            _mqttClient = new MqttClient(
                broker, port, 
                true, // 启用SSL(MQTTS关键配置)
                MqttSslProtocols.None, null, null
            );

            // 绑定消息接收回调
            _mqttClient.MqttMsgPublishReceived += client_MqttMsgPublishReceived;

            // 连接服务器(带用户名密码)
            byte connackCode = _mqttClient.Connect(_clientId, leUsername.Text, lePassword.Text);

            if (_mqttClient.IsConnected)
            {
                _isConnected = true;
                btnConnect.Text = "断开服务器";
                btnSubscribe.Enabled = btnPublish.Enabled = true;
                AddLog("✅ 成功连接MQTTS服务器");
            }
            else
            {
                // 解析连接失败原因(兼容C# 7.3的switch语句)
                string reason;
                switch (connackCode)
                {
                    case 1: reason = "不支持的协议版本"; break;
                    case 2: reason = "客户端ID无效"; break;
                    case 4: reason = "用户名/密码错误"; break;
                    default: reason = $"未知原因(返回码:{connackCode})"; break;
                }
                AddLog($"❌ 连接失败:{reason}", true);
            }
        }
        catch (Exception ex)
        {
            AddLog($"❌ 连接异常:{ex.Message}", true);
        }
    }
    else
    {
        // 断开连接
        _mqttClient.Disconnect();
        _isConnected = false;
        btnConnect.Text = "连接服务器";
        AddLog("❌ 已断开MQTTS连接");
    }
}

4.3 主题订阅与取消订阅

通过btnSubscribe_Click实现主题订阅,同时记录订阅信息到ListView和字典;通过btnClose_Click取消单个订阅:

private void btnSubscribe_Click(object sender, EventArgs e)
{
    string topic = leSubTopic.Text.Trim();
    byte qos = MqttMsgBase.QOS_LEVEL_AT_MOST_ONCE; // QoS 0

    if (string.IsNullOrWhiteSpace(topic) || !_isConnected)
    {
        AddLog("请输入主题并确保已连接", true);
        return;
    }

    if (_subscribedTopics.ContainsKey(topic))
    {
        MessageBox.Show("已订阅该主题");
        return;
    }

    // 执行订阅
    _mqttClient.Subscribe(new[] { topic }, new byte[] { qos });
    _subscribedTopics.Add(topic, qos);

    // 添加到ListView显示
    ListViewItem item = new ListViewItem((listViewTopics.Items.Count + 1).ToString());
    item.SubItems.Add(topic);
    item.SubItems.Add(qos.ToString());
    item.Tag = topic; // 存储主题用于取消订阅
    listViewTopics.Items.Add(item);
    AddLog($"✅ 订阅主题:{topic}");
}

// 取消单个订阅
private void btnClose_Click(object sender, EventArgs e)
{
    if (listViewTopics.SelectedItems.Count == 0) return;

    string topic = listViewTopics.SelectedItems[0].Tag.ToString();
    _mqttClient.Unsubscribe(new[] { topic });
    _subscribedTopics.Remove(topic);
    listViewTopics.Items.Remove(listViewTopics.SelectedItems[0]);
    AddLog($"❌ 取消订阅:{topic}");
}

4.4 消息发布与设备控制

通过btnPublish_Click发布自定义消息,通过sen_ctrl封装设备控制指令(如 LED 开关):

private void btnPublish_Click(object sender, EventArgs e)
{
    string topic = lePubTopic.Text.Trim();
    string msg = lePubMessage.Text.Trim();
    if (string.IsNullOrWhiteSpace(topic) || string.IsNullOrWhiteSpace(msg))
    {
        AddLog("主题或消息不能为空", true);
        return;
    }

    // 发布消息(UTF8编码)
    byte[] msgBytes = Encoding.UTF8.GetBytes(msg);
    _mqttClient.Publish(topic, msgBytes);
    AddLog($"📤 发布到{topic}:{msg}");
}

// 设备控制指令封装
private void sen_ctrl(string cmd)
{
    string topic = lePubTopic.Text.Trim();
    if (!_isConnected || string.IsNullOrWhiteSpace(topic)) return;

    _mqttClient.Publish(topic, Encoding.UTF8.GetBytes(cmd));
    AddLog($"📤 发送指令:{cmd}");
}

// LED开启按钮
private void led_open_Click(object sender, EventArgs e)
{
    sen_ctrl("led on");
}

4.5 消息接收与跨线程 UI 更新

MQTT 消息接收在独立线程,需通过Invoke跨线程更新 WinForms 控件:

static void client_MqttMsgPublishReceived(object sender, MqttMsgPublishEventArgs e)
{
    // 解析设备消息(UTF8避免中文乱码)
    string payload = Encoding.UTF8.GetString(e.Message);
    string log = $"📩 收到[{e.Topic}]:{payload}";

    // 跨线程更新日志(获取当前窗体实例)
    Form1 form = Application.OpenForms.OfType<Form1>().FirstOrDefault();
    if (form != null)
    {
        form.AddLog(log);
    }
}

// 跨线程安全的日志添加方法
public void AddLog(string log, bool isError = false)
{
    if (teLogDisplay.InvokeRequired)
    {
        teLogDisplay.Invoke(new Action<string, bool>(AddLog), log, isError);
        return;
    }

    // 更新日志控件
    teLogDisplay.AppendText($"[{DateTime.Now:HH:mm:ss}] {log}\n");
    teLogDisplay.ScrollToCaret();
    teLogDisplay.SelectionColor = isError ? Color.Red : Color.Black;
}

4.6 日志导出

通过SaveFileDialog实现日志导出为本地 TXT 文件:

private void btnExportLog_Click(object sender, EventArgs e)
{
    if (string.IsNullOrWhiteSpace(teLogDisplay.Text))
    {
        MessageBox.Show("日志为空,无需导出");
        return;
    }

    using (SaveFileDialog sfd = new SaveFileDialog())
    {
        sfd.FileName = $"MQTT日志_{DateTime.Now:yyyyMMddHHmmss}.txt";
        sfd.Filter = "文本文档|*.txt";
        if (sfd.ShowDialog() == DialogResult.OK)
        {
            File.WriteAllText(sfd.FileName, teLogDisplay.Text, Encoding.UTF8);
            AddLog($"📥 日志导出到:{sfd.FileName}");
        }
    }
}

五、界面展示

服务器端可参考我上一篇文章基于 RT-Thread Studio 实战:ESP8266+MQTT-优快云博客

六、后续功能扩展建议

  • 添加自动重连逻辑,应对网络波动;
  • 保存服务器配置到本地(如 INI/JSON 文件);
  • 支持多设备主题管理;
  • 增加消息解析与可视化(如显示传感器数据图表)。

该客户端可直接与 STM32 等嵌入式设备的 MQTT 客户端通信,是物联网上位机开发的实用模板。

仓库地址:MQTT_Client: 基于C#语言一个客户端程序,实现和嵌入式设备相互通信。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值