.NET之基于MQTTnet 实现MQTT服务器和客户端


前面说到MQTT技术介绍

现在物联网的概念很火,就工业智能互联来说,水平方向上如何把流水线式的孤岛式机台联动起来,我们比较有经验,但是垂直方向上,如何做数采,或者说如何高效灵活的做数采,需要补课的东西还有很多。MQTT是IBM很早以前就提出来的协议,但很可惜一直没有接触过,新公司的项目上引用了MQTTnet的开源库

 

官网指路:https://mqtt.org/

MQTTnet


MQTTnet是基于MQTT通信的高性能.NET库,它提供了一个MQTT客户端和一个MQTT服务器(代理)。支持.net core,支持MQTT 3.X和5.0版本。

https://github.com/chkr1011/MQTTnet
MQTTnet的Git路径。

 

正文

 

本Demo设计为一个WPF程序。基于MQTTnet,实现了一个MQTT Server或者说Broker的创建,同时在窗体上提供了MQTT Client的创建功能,MQTT Client跟Server连接之后,通过点击按钮,实现主题订阅、发布的基础功能。

并可以通过MQTTX工具调试。

 

 

 

 

MQTT Server/Broker实现

创建MQTT Server的思路还是蛮清晰的,主要是MqttServer各个事件的实现。

 

MQTT Server/Broker创建

先定义好MqttServerOptions,这是启动Mqtt服务时候的传参,定义服务启动时的各种参数:IP、Port口,账密等等

var optionsBuilder = new MqttServerOptionsBuilder();

 

实例化MqttServer以及委托实现MqttServer的各个事件

mqttServer = new MqttFactory().CreateMqttServer() as MqttServer;
mqttServer.StartedHandler = new MqttServerStartedHandlerDelegate(OnMqttServerStarted);
mqttServer.StoppedHandler = new MqttServerStoppedHandlerDelegate(OnMqttServerStopped);

  

下面是完成的实现代码:

public MqttServer mqttServer = null;
public async void StartMqttServer()
{
try
{
if (mqttServer == null)
{
var config = ReadConfiguration();
var optionsBuilder = new MqttServerOptionsBuilder()
.WithDefaultEndpoint().WithDefaultEndpointPort(int.Parse(config["Port"].ToString())).WithConnectionValidator(
c =>
{
var currentUser = config["Users"][0]["UserName"].ToString();
var currentPWD = config["Users"][0]["Password"].ToString();

if (currentUser == null || currentPWD == null)
{
c.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
return;
}

if (c.Username != currentUser)
{
c.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
return;
}

if (c.Password != currentPWD)
{
c.ReasonCode = MqttConnectReasonCode.BadUserNameOrPassword;
return;
}

c.ReasonCode = MqttConnectReasonCode.Success;
}).WithSubscriptionInterceptor(
c =>
{
c.AcceptSubscription = true;
}).WithApplicationMessageInterceptor(
c =>
{
c.AcceptPublish = true;
});

mqttServer = new MqttFactory().CreateMqttServer() as MqttServer;
mqttServer.StartedHandler = new MqttServerStartedHandlerDelegate(OnMqttServerStarted);
mqttServer.StoppedHandler = new MqttServerStoppedHandlerDelegate(OnMqttServerStopped);

mqttServer.ClientConnectedHandler = new MqttServerClientConnectedHandlerDelegate(OnMqttServerClientConnected);
mqttServer.ClientDisconnectedHandler = new MqttServerClientDisconnectedHandlerDelegate(OnMqttServerClientDisconnected);
mqttServer.ClientSubscribedTopicHandler = new MqttServerClientSubscribedHandlerDelegate(OnMqttServerClientSubscribedTopic);
mqttServer.ClientUnsubscribedTopicHandler = new MqttServerClientUnsubscribedTopicHandlerDelegate(OnMqttServerClientUnsubscribedTopic);
mqttServer.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(OnMqttServer_ApplicationMessageReceived);


await mqttServer.StartAsync(optionsBuilder.Build());
lbxMonitor.BeginInvoke(_updateMonitorAction,
Logger.TraceLog(Logger.Level.Info, "MQTT Server is started."));

}
}
catch (Exception ex)
{
lbxMonitor.BeginInvoke(_updateMonitorAction,
Logger.TraceLog(Logger.Level.Fatal, $"MQTT Server start fail.>{ex.Message}"));
}
}

public async void StopMqttServer()
{
if (mqttServer == null) return;
try
{
await mqttServer?.StopAsync();
mqttServer = null;
lbxMonitor.BeginInvoke(_updateMonitorAction,
Logger.TraceLog(Logger.Level.Info, "MQTT Server is stopped."));
}
catch (Exception ex)
{
lbxMonitor.BeginInvoke(_updateMonitorAction,
Logger.TraceLog(Logger.Level.Fatal, $"MQTT Server stop fail.>{ex.Message}"));
}
}

  

MQTT Server/Broker发布消息

从MQTT的设计来看,服务端是代理的角色,订阅者和发布者是客户端,所以通常来说,消息的订阅与发布应当都是客户端干的事。但是,服务端自然也是可以参与一下发布者的角色的。我还没想到这样用的实际场景,先功能实现一下——

很粗暴,先实例化一个MqttApplicationMessage对象,然后作为传参调用MqttServer.PublishAsync进行消息发布。

public async void ServerPublishMqttTopic(string topic, string payload)
{
var message = new MqttApplicationMessage()
{
Topic = topic,
Payload = Encoding.UTF8.GetBytes(payload)
};
await mqttServer.PublishAsync(message);
lbxMonitor.BeginInvoke(_updateMonitorAction,
Logger.TraceLog(Logger.Level.Info, string.Format("MQTT Broker发布主题[{0}]成功!", topic)));
}

 


当然啦,MqttApplicationMessage还有很多属性值可以传,QoS(MqttQualityOfServiceLevel)、Retain等等,这里只是为了先实现功能,就只传了最简单的Topic和Payload。

 

MQTT Client创建


整体的实现思路跟Server端如出一辙,声明一个MqttClientOptions,赋值各种连接Server端需要的参数,最后作为MqttClient.ConnectAsync的传参,连接Server。
这里我在MqttClientOptions里面尝试了一下WillMessage,其实就是一个MqttApplicationMessage对象,WillMessage作为遗言机制,用于Client跟Server端挂点时的广播通知。
再之后就是实现MqttClient的各个事件,用来客制Connected,Disconnected,MessageReceived的各种逻辑,跟Server端实现没有什么区别,不再赘述。

private MqttClient mqttClient = null;
private async Task ClientStart()
{
try
{
var tcpServer = txtIPAddr.Text;
var tcpPort = int.Parse(txtPort.Text.Trim());
var mqttUser = txtUserName.Text.Trim();
var mqttPassword = txtPWD.Text.Trim();

var mqttFactory = new MqttFactory();

 

var options = new MqttClientOptions
{
ClientId = txtClientID.Text.Trim(),
ProtocolVersion = MQTTnet.Formatter.MqttProtocolVersion.V311,
ChannelOptions = new MqttClientTcpOptions
{
Server = tcpServer,
Port = tcpPort
},
WillDelayInterval = 10,
WillMessage = new MqttApplicationMessage()
{
Topic = $"LastWill/{txtClientID.Text.Trim()}",
Payload= Encoding.UTF8.GetBytes("I Lost the connection!"),
QualityOfServiceLevel = MqttQualityOfServiceLevel.ExactlyOnce
}

};
if (options.ChannelOptions == null)
{
throw new InvalidOperationException();
}

if (!string.IsNullOrEmpty(mqttUser))
{
options.Credentials = new MqttClientCredentials
{
Username = mqttUser,
Password = Encoding.UTF8.GetBytes(mqttPassword)
};
}

options.CleanSession = true;
options.KeepAlivePeriod = TimeSpan.FromSeconds(5);

mqttClient = mqttFactory.CreateMqttClient() as MqttClient;
mqttClient.ConnectedHandler = new MqttClientConnectedHandlerDelegate(OnMqttClientConnected);
mqttClient.DisconnectedHandler = new MqttClientDisconnectedHandlerDelegate(OnMqttClientDisConnected);
mqttClient.ApplicationMessageReceivedHandler = new MqttApplicationMessageReceivedHandlerDelegate(OnSubscriberMessageReceived);
await mqttClient.ConnectAsync(options);
lbxMonitor.BeginInvoke(_updateMonitorAction,
Logger.TraceLog(Logger.Level.Info, $"客户端[{options.ClientId}]尝试连接..."));
}
catch (Exception ex)
{
lbxMonitor.BeginInvoke(_updateMonitorAction,
Logger.TraceLog(Logger.Level.Fatal, $"客户端尝试连接出错.>{ex.Message}"));
}
}

private async Task ClientStop()
{
try
{
if (mqttClient == null) return;
await mqttClient.DisconnectAsync();
mqttClient = null;
}
catch (Exception ex)
{
lbxMonitor.BeginInvoke(_updateMonitorAction,
Logger.TraceLog(Logger.Level.Fatal, $"客户端尝试断开Server出错.>{ex.Message}"));
}
}

  

MQTT Client发布消息
这里的实现逻辑跟写法和Server端的发布别无二致,我在这里的MqttApplicationMessage补上了QoS和Retain的设置,由Form页面的控件传参。
这里补一句关于Retain的用法:Retain意为保留,设为True表示这条消息发布的时候如果没有订阅者,则该消息保留在Server端,直到被人订阅时立刻发布出去并删除,设为False时则没有这样的效果。

public async void ClientPublishMqttTopic(string topic, string payload)
{
try
{
var message = new MqttApplicationMessage()
{
Topic = topic,
Payload = Encoding.UTF8.GetBytes(payload),
QualityOfServiceLevel = (MqttQualityOfServiceLevel)cmbQos.SelectedIndex,
Retain = bool.Parse(cmbRetain.SelectedItem.ToString())
};
await mqttClient.PublishAsync(message);
lbxMonitor.BeginInvoke(_updateMonitorAction,
Logger.TraceLog(Logger.Level.Info, string.Format("客户端[{0}]发布主题[{1}]成功!", mqttClient.Options.ClientId, topic)));
}
catch (Exception ex)
{
lbxMonitor.BeginInvoke(_updateMonitorAction,
Logger.TraceLog(Logger.Level.Fatal, string.Format("客户端[{0}]发布主题[{1}]异常!>{2}", mqttClient.Options.ClientId, topic,ex.Message)));
}
}

  


MQTT Clien订阅消息
呼叫MqttClient.SubscribeAsync,传入消息主题即可。

public async void ClientSubscribeTopic(string topic)
{
await mqttClient.SubscribeAsync(topic);
lbxMonitor.BeginInvoke(_updateMonitorAction,
Logger.TraceLog(Logger.Level.Info, string.Format("客户端[{0}]订阅主题[{1}]成功!", mqttClient.Options.ClientId, topic)));
}

  


MQTT Clien取消订阅消息
呼叫MqttClient.UnsubscribeAsync,取消消息订阅。

public async void ClientUnSubscribeTopic(string topic)
{
await mqttClient.UnsubscribeAsync(topic);
lbxMonitor.BeginInvoke(_updateMonitorAction,
Logger.TraceLog(Logger.Level.Info, string.Format("客户端[{0}]取消主题[{1}]成功!", mqttClient.Options.ClientId, topic)));
}

以上代码是参考

总代码

服务端

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using MahApps.Metro.Controls;
using System.ComponentModel;
using MQTTnet.Server;
using MQTTnet.Adapter;
using MQTTnet.Protocol;
using MQTTnet;
using System.Collections.ObjectModel;

namespace MqttDemo.WPFServer
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : MetroWindow
    {
        private MainWindowModel _model;
        private IMqttServer server;

        public MainWindow()
        {
            InitializeComponent();
            _model = new MainWindowModel();
            this.DataContext = _model;
        }

        #region 启动按钮事件
        private async void btnStart_Click(object sender, RoutedEventArgs e)
        {
            var optionBuilder = new MqttServerOptionsBuilder().WithDefaultEndpointBoundIPAddress(System.Net.IPAddress.Parse(_model.HostIP)).WithDefaultEndpointPort(_model.HostPort).WithDefaultCommunicationTimeout(TimeSpan.FromMilliseconds(_model.Timeout)).WithConnectionValidator(t =>
            {
                if (t.Username!=_model.UserName||t.Password!=_model.Password)
                {
                    t.ReturnCode = MqttConnectReturnCode.ConnectionRefusedBadUsernameOrPassword;
                }
                t.ReturnCode = MqttConnectReturnCode.ConnectionAccepted;
            });
            var option = optionBuilder.Build();

            server = new MqttFactory().CreateMqttServer();
            server.ApplicationMessageReceived += Server_ApplicationMessageReceived;//绑定消息接收事件
            server.ClientConnected += Server_ClientConnected;//绑定客户端连接事件
            server.ClientDisconnected += Server_ClientDisconnected;//绑定客户端断开事件
            server.ClientSubscribedTopic += Server_ClientSubscribedTopic;//绑定客户端订阅主题事件
            server.ClientUnsubscribedTopic += Server_ClientUnsubscribedTopic;//绑定客户端退订主题事件
            server.Started += Server_Started;//绑定服务端启动事件
            server.Stopped += Server_Stopped;//绑定服务端停止事件
            //启动
            await server.StartAsync(option);

        }


        #endregion

        #region 停止按钮事件
        private async void btnStop_Click(object sender, RoutedEventArgs e)
        {
            if (server != null)
            {
                await server.StopAsync();
            }
        }
        #endregion

        #region 服务端停止事件
        private void Server_Stopped(object sender, EventArgs e)
        {
            WriteToStatus("服务端已停止!");
        }
        #endregion

        #region 服务端启动事件
        private void Server_Started(object sender, EventArgs e)
        {
            WriteToStatus("服务端已启动!");
        }
        #endregion

        #region 客户端退订主题事件
        private void Server_ClientUnsubscribedTopic(object sender, MqttClientUnsubscribedTopicEventArgs e)
        {
            this.Dispatcher.Invoke(() =>
            {
                if (_model.AllTopics.Any(t => t.Topic == e.TopicFilter))
                {
                    TopicModel model = _model.AllTopics.First(t => t.Topic == e.TopicFilter);
                    _model.AllTopics.Remove(model);
                    model.Clients.Remove(e.ClientId);
                    model.Count--;
                    if (model.Count > 0)
                    {
                        _model.AllTopics.Add(model);
                    }
                }
            });
            WriteToStatus("客户端" + e.ClientId + "退订主题" + e.TopicFilter);
        }
        #endregion

        #region 客户端订阅主题事件
        private void Server_ClientSubscribedTopic(object sender, MqttClientSubscribedTopicEventArgs e)
        {
            this.Dispatcher.Invoke(() =>
            {
                if (_model.AllTopics.Any(t => t.Topic == e.TopicFilter.Topic))
                {
                    TopicModel model = _model.AllTopics.First(t => t.Topic == e.TopicFilter.Topic);
                    _model.AllTopics.Remove(model);
                    model.Clients.Add(e.ClientId);
                    model.Count++;
                    _model.AllTopics.Add(model);
                }
                else
                {
                    TopicModel model = new TopicModel(e.TopicFilter.Topic, e.TopicFilter.QualityOfServiceLevel)
                    {
                        Clients = new List<string> { e.ClientId },
                        Count = 1
                    };
                    _model.AllTopics.Add(model);
                }
            });

            WriteToStatus("客户端" + e.ClientId + "订阅主题" + e.TopicFilter.Topic);
        }
        #endregion

        #region 客户端断开事件
        private void Server_ClientDisconnected(object sender, MqttClientDisconnectedEventArgs e)
        {
            this.Dispatcher.Invoke(() =>
            {
                _model.AllClients.Remove(e.ClientId);
                var query = _model.AllTopics.Where(t => t.Clients.Contains(e.ClientId));
                if (query.Any())
                {
                    var tmp = query.ToList();
                    foreach (var model in tmp)
                    {
                        _model.AllTopics.Remove(model);
                        model.Clients.Remove(e.ClientId);
                        model.Count--;
                        _model.AllTopics.Add(model);
                    }
                }
            });


            WriteToStatus("客户端" + e.ClientId + "断开");
        }
        #endregion

        #region 客户端连接事件
        private void Server_ClientConnected(object sender, MqttClientConnectedEventArgs e)
        {
            this.Dispatcher.Invoke(() =>
            {
                _model.AllClients.Add(e.ClientId);
            });
            WriteToStatus("客户端" + e.ClientId + "连接");
        }
        #endregion

        #region 收到消息事件
        private void Server_ApplicationMessageReceived(object sender, MqttApplicationMessageReceivedEventArgs e)
        {
            if (e.ApplicationMessage.Topic == "/environ/temp")
            {
                string str = System.Text.Encoding.UTF8.GetString(e.ApplicationMessage.Payload);
                double tmp;
                bool isdouble = double.TryParse(str, out tmp);
                if (isdouble)
                {
                    string result = "";
                    if (tmp > 40)
                    {
                        result = "温度过高!";
                    }
                    else if (tmp < 10)
                    {
                        result = "温度过低!";
                    }
                    else
                    {
                        result = "温度正常!";
                    }
                    MqttApplicationMessage message = new MqttApplicationMessage()
                    {
                        Topic = e.ApplicationMessage.Topic,
                        Payload = Encoding.UTF8.GetBytes(result),
                        QualityOfServiceLevel = e.ApplicationMessage.QualityOfServiceLevel,
                        Retain = e.ApplicationMessage.Retain
                    };
                    server.PublishAsync(message);
                }
            }
            WriteToStatus("收到消息" + e.ApplicationMessage.ConvertPayloadToString() + ",来自客户端" + e.ClientId + ",主题为" + e.ApplicationMessage.Topic);
        }
        #endregion

        #region 打开配置页面
        private void btnConfig_Click(object sender, RoutedEventArgs e)
        {
            this.flyConfig.IsOpen = !this.flyConfig.IsOpen;
        }
        #endregion

        #region 增加主题按钮事件
        private void btnAddTopic_Click(object sender, RoutedEventArgs e)
        {
            if (!string.IsNullOrWhiteSpace(_model.AddTopic)&&server!=null)
            {
                TopicModel topic = new TopicModel(_model.AddTopic, MqttQualityOfServiceLevel.AtLeastOnce);
                foreach (string clientId in _model.AllClients)
                {
                    server.SubscribeAsync(clientId, new List<TopicFilter> { topic });
                }
                _model.AllTopics.Add(topic);
            }
        }
        #endregion

        #region 清理内容
        private void menuClear_Click(object sender, RoutedEventArgs e)
        {
            txtRich.Document.Blocks.Clear();
        }
        #endregion

        #region 状态输出
        /// <summary>
        /// 状态输出
        /// </summary>
        /// <param name="message"></param>
        public void WriteToStatus(string message)
        {
            if (!(txtRich.CheckAccess()))
            {
                this.Dispatcher.Invoke(() =>
                    WriteToStatus(message)
                    );
                return;
            }
            string strTime = "[" + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "] ";
            txtRich.AppendText(strTime + message + "\r");

        }
        #endregion


    }

    public class MainWindowModel : INotifyPropertyChanged
    {
        public MainWindowModel()
        {
            hostIP = "127.0.0.1";//绑定的IP地址
            hostPort = 61613;//绑定的端口号12345
            timeout = 3000;//连接超时时间
            username = "admin";//用户名
            password = "password";//密码
            allTopics = new ObservableCollection<TopicModel>();//主题
            allClients = new ObservableCollection<string>();//客户端
            addTopic = "";
        }

        private ObservableCollection<TopicModel> allTopics;

        public ObservableCollection<TopicModel> AllTopics
        {
            get { return allTopics; }
            set
            {
                if (allTopics != value)
                {
                    allTopics = value;
                    this.OnPropertyChanged("AllTopics");
                }

            }
        }

        private ObservableCollection<string> allClients;

        public ObservableCollection<string> AllClients
        {
            get { return allClients; }
            set
            {
                if (allClients != value)
                {
                    allClients = value;
                    this.OnPropertyChanged("AllClients");
                }

            }
        }


        private string hostIP;

        public string HostIP
        {
            get { return hostIP; }
            set
            {
                if (hostIP != value)
                {
                    hostIP = value;
                    this.OnPropertyChanged("HostIP");
                }

            }
        }

        private int hostPort;

        public int HostPort
        {
            get { return hostPort; }
            set
            {
                if (hostPort != value)
                {
                    hostPort = value;
                    this.OnPropertyChanged("HostPort");
                }

            }
        }

        private int timeout;

        public int Timeout
        {
            get { return timeout; }
            set
            {
                if (timeout != value)
                {
                    timeout = value;
                    this.OnPropertyChanged("Timeout");
                }

            }
        }

        private string username;

        public string UserName
        {
            get { return username; }
            set
            {
                if (username != value)
                {
                    username = value;
                    this.OnPropertyChanged("UserName");
                }

            }
        }


        private string password;

        public string Password
        {
            get { return password; }
            set
            {
                if (password != value)
                {
                    password = value;
                    this.OnPropertyChanged("Password");
                }

            }
        }

        private string addTopic;

        public string AddTopic
        {
            get { return addTopic; }
            set
            {
                if (addTopic != value)
                {
                    addTopic = value;
                    this.OnPropertyChanged("AddTopic");
                }

            }
        }


        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public class TopicModel : TopicFilter,INotifyPropertyChanged
    {
        public TopicModel(string topic, MqttQualityOfServiceLevel qualityOfServiceLevel) : base(topic, qualityOfServiceLevel)
        {
            clients = new List<string>();
            count = 0;
        }

        private int count;
        /// <summary>
        /// 订阅此主题的客户端数量
        /// </summary>
        public int Count
        {
            get { return count; }
            set
            {
                if (count != value)
                {
                    count = value;
                    this.OnPropertyChanged("Count");
                }

            }
        }

        private List<string> clients;
        /// <summary>
        /// 订阅此主题的客户端
        /// </summary>
        public List<string> Clients
        {
            get { return clients; }
            set
            {
                if (clients != value)
                {
                    clients = value;
                    this.OnPropertyChanged("Clients");
                }

            }
        }


        protected virtual void OnPropertyChanged(string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
        public event PropertyChangedEventHandler PropertyChanged;
    }
}

  

客户端:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using MahApps.Metro.Controls;
using MQTTnet;
using MQTTnet.Client;
using System.ComponentModel;
using MahApps.Metro.Controls.Dialogs;

namespace MqttDemo.MetroClient
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : MetroWindow
    {
        private MainWindowModel _model;
        private IMqttClient _client;
        public MainWindow()
        {
            InitializeComponent();
            _model = new MainWindowModel()
            {
                AllTopics = InitTopics(),
                SelectedTopics = new List<TopicFilter>(),
                ServerUri = "127.0.0.1",
                CurrentTopic = null,
                ServerPort=61613,
                ClientID = Guid.NewGuid().ToString("N")
            };
            this.DataContext = _model;
        }

        #region 订阅主题面板
        /// <summary>
        /// 打开订阅主题面板
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnSub_Click(object sender, RoutedEventArgs e)
        {
            this.flySub.IsOpen = !this.flySub.IsOpen;
        }
        #endregion

        #region 用户配置面板
        private void btnLogin_Click(object sender, RoutedEventArgs e)
        {
            this.flyLogin.IsOpen = !this.flyLogin.IsOpen;
        }
        #endregion

        #region 数据初始化
        /// <summary>
        /// 数据初始化
        /// </summary>
        /// <returns></returns>
        private List<TopicModel> InitTopics()
        {
            List<TopicModel> topics = new List<TopicModel>();
            topics.Add(new TopicModel("/environ/temp", "环境-温度"));
            topics.Add(new TopicModel("/environ/hum", "环境-湿度"));
            //topics.Add(new TopicModel("/environ/pm25", "环境-PM2.5"));
            //topics.Add(new TopicModel("/environ/CO2", "环境-二氧化碳"));
            //topics.Add(new TopicModel("/energy/electric", "能耗-电"));
            //topics.Add(new TopicModel("/energy/water", "环境-水"));
            //topics.Add(new TopicModel("/energy/gas", "环境-电"));
            topics.Add(new TopicModel("/data/alarm", "数据-报警"));
            topics.Add(new TopicModel("/data/message", "数据-消息"));
            topics.Add(new TopicModel("/data/notify", "数据-通知"));
            return topics;
        }

        /// <summary>
        /// 数据模型转换
        /// </summary>
        /// <param name="topics"></param>
        /// <returns></returns>
        private List<TopicFilter> ConvertTopics(List<TopicModel> topics)
        {
            //MQTTnet.TopicFilter
            List<TopicFilter> filters = new List<TopicFilter>();
            foreach (TopicModel model in topics)
            {
                TopicFilter filter = new TopicFilter(model.Topic,MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce);
                filters.Add(filter);

            }
            return filters;
        }
        #endregion

        #region 状态输出
        /// <summary>
        /// 状态输出
        /// </summary>
        /// <param name="message"></param>
        public void WriteToStatus(string message)
        {
            if (!(txtRich.CheckAccess()))
            {
                this.Dispatcher.Invoke(() =>
                    WriteToStatus(message)
                    );
                return;
            }
            string strTime = "[" + System.DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss") + "] ";
            txtRich.AppendText(strTime + message + "\r");
            if (txtRich.ExtentHeight > 200)
            {
                txtRich.Document.Blocks.Clear();
            }
        }
        #endregion

        #region 更改订阅的主题
        /// <summary>
        /// 保存订阅的主题
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnSave_Click(object sender, RoutedEventArgs e)
        {
            List<TopicModel> topics = _model.AllTopics.Where(t => t.IsSelected == true).ToList();

            _model.SelectedTopics = ConvertTopics(topics);
            this.flySub.IsOpen = !this.flySub.IsOpen;
            SubscribeTopics(_model.SelectedTopics);
        }
        #endregion

        #region 订阅主题
        private void SubscribeTopics(List<TopicFilter> filters)
        {
            if (_client!=null)
            {
                _client.SubscribeAsync(filters);
                string tmp = "";
                foreach (var filter in filters)
                {
                    tmp += filter.Topic;
                    tmp += ",";
                }
                if (tmp.Length>1)
                {
                    tmp = tmp.Substring(0, tmp.Length - 1);
                }
                WriteToStatus("成功订阅主题:"+tmp);
            }
            else
            {
                ShowDialog("提示", "请连接服务端后订阅主题!");
            }
        }
        #endregion

        #region 连接/断开服务端
        /// <summary>
        /// 连接服务端
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnStart_Click(object sender, RoutedEventArgs e)
        {
            if (_model.ServerUri!=null&&_model.ServerPort>0)
            {
                InitClient(_model.ClientID, _model.ServerUri, _model.ServerPort);
            }
            else
            {
                ShowDialog("提示", "服务端地址或端口号不能为空!");
            }
        }
        /// <summary>
        /// 断开服务端
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnStop_Click(object sender, RoutedEventArgs e)
        {
            if (_client != null)
            {
                _client.DisconnectAsync();
            }
        }
        #endregion

        #region MQTT方法
        /// <summary>
        /// 初始化
        /// </summary>
        /// <param name="id"></param>
        /// <param name="url"></param>
        /// <param name="port"></param>
        private void InitClient(string id,string url = "127.0.0.1", int port = 1883)
        {
            var options = new MqttClientOptions()
            {
                ClientId = id
            };
            options.ChannelOptions = new MqttClientTcpOptions()
            {
                Server = url,
                Port = port
            };
            options.Credentials = new MqttClientCredentials()
            {
                Username=_model.UserName,
                Password=_model.Password
            };
            options.CleanSession = true;
            options.KeepAlivePeriod = TimeSpan.FromSeconds(100);
            options.KeepAliveSendInterval = TimeSpan.FromSeconds(10000);
            if (_client != null)
            {
                _client.DisconnectAsync();
                _client = null;
            }
            _client = new MQTTnet.MqttFactory().CreateMqttClient();
            _client.ApplicationMessageReceived += _client_ApplicationMessageReceived;
            _client.Connected += _client_Connected;
            _client.Disconnected += _client_Disconnected;
            _client.ConnectAsync(options);
        }

        /// <summary>
        /// 客户端与服务端断开连接
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void _client_Disconnected(object sender, MqttClientDisconnectedEventArgs e)
        {
            _model.IsConnected = false;
            _model.IsDisConnected = true;
            WriteToStatus("与服务端断开连接!");
        }

        /// <summary>
        /// 客户端与服务端建立连接
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>

        private void _client_Connected(object sender, MqttClientConnectedEventArgs e)
        {
            _model.IsConnected = true;
            _model.IsDisConnected = false;
            WriteToStatus("与服务端建立连接");
        }

        /// <summary>
        /// 客户端收到消息
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void _client_ApplicationMessageReceived(object sender, MQTTnet.MqttApplicationMessageReceivedEventArgs e)
        {
            WriteToStatus("收到来自客户端" + e.ClientId + ",主题为" + e.ApplicationMessage.Topic + "的消息:" + Encoding.UTF8.GetString(e.ApplicationMessage.Payload));
        }
        #endregion

        #region 发布消息
        /// <summary>
        /// 发布主题
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void btnPublish_Click(object sender, RoutedEventArgs e)
        {
            if (_client!=null)
            {
                if (this.comboTopics.SelectedIndex<0)
                {
                    ShowDialog("提示", "请选择要发布的主题!");
                    return;
                }
                if (string.IsNullOrEmpty(txtContent.Text))
                {
                    ShowDialog("提示", "消息内容不能为空!");
                    return;
                }
                string topic = comboTopics.SelectedValue as string;
                string content = txtContent.Text;
                MqttApplicationMessage msg = new MqttApplicationMessage
                {
                    Topic=topic,
                    Payload=Encoding.UTF8.GetBytes(content),
                    QualityOfServiceLevel=MQTTnet.Protocol.MqttQualityOfServiceLevel.AtLeastOnce,
                    Retain=false
                };
                _client.PublishAsync(msg);
                WriteToStatus("成功发布主题为" + topic+"的消息!");
            }
            else
            {
                ShowDialog("提示", "请连接服务端后发布消息!");
                return;
            }
        }
        #endregion

        #region 提示框
        public void ShowDialog(string title, string content)
        {
            _showDialog(title, content);
        }
        /// <summary>
        /// 提示框
        /// </summary>
        /// <param name="title"></param>
        /// <param name="content"></param>
        private void _showDialog(string title, string content)
        {
            var mySetting = new MetroDialogSettings()
            {
                AffirmativeButtonText = "确定",
                //NegativeButtonText = "Go away!",
                //FirstAuxiliaryButtonText = "Cancel",
                ColorScheme = this.MetroDialogOptions.ColorScheme
            };

            MessageDialogResult result = this.ShowModalMessageExternal(title, content, MessageDialogStyle.Affirmative, mySetting);
        }



        #endregion

        private void btnSaveConfig_Click(object sender, RoutedEventArgs e)
        {

        }
    }

    public class MainWindowModel: INotifyPropertyChanged
    {
        private List<TopicModel> _allTopics;

        public List<TopicModel> AllTopics
        {
            get { return _allTopics; }
            set
            {
                if (_allTopics!=value)
                {
                    _allTopics = value;
                    OnPropertyChanged("AllTopics");
                }
            }
        }

        private List<TopicFilter> _selectedTopics;

        public List<TopicFilter> SelectedTopics
        {
            get { return _selectedTopics; }
            set
            {
                if (_selectedTopics!=value)
                {
                    _selectedTopics = value;
                    OnPropertyChanged("SelectedTopics");
                }
            }
        }

        private string _serverUri;

        public string ServerUri
        {
            get { return _serverUri; }
            set
            {
                if (_serverUri!=value)
                {
                    _serverUri = value;
                    OnPropertyChanged("ServerUri");
                }
            }
        }

        private int _serverPort;

        public int ServerPort
        {
            get { return _serverPort; }
            set
            {
                if (_serverPort!=value)
                {
                    _serverPort = value;
                    OnPropertyChanged("ServerPort");
                }
            }
        }


        private string _clientId;

        public string ClientID
        {
            get { return _clientId; }
            set
            {
                if (_clientId!=value)
                {
                    _clientId = value;
                    OnPropertyChanged("ClientID");
                }
            }
        }

        private TopicFilter _currentTopic;

        public TopicFilter CurrentTopic
        {
            get { return _currentTopic; }
            set
            {
                if (_currentTopic!=value)
                {
                    _currentTopic = value;
                    OnPropertyChanged("CurrentTopic");
                }
            }
        }

        private bool? _isConnected=false;

        public bool? IsConnected
        {
            get { return _isConnected; }
            set
            {
                if (_isConnected!=value)
                {
                    _isConnected = value;
                    OnPropertyChanged("IsConnected");
                }
            }
        }

        private bool _isDisConnected=true;

        public bool IsDisConnected
        {
            get { return _isDisConnected; }
            set
            {
                if (_isDisConnected != value)
                {
                    _isDisConnected = value;
                    this.OnPropertyChanged("IsDisConnected");
                }
            }
        }

        private string _userName="admin";

        public string UserName
        {
            get { return _userName; }
            set
            {
                if (_userName != value)
                {
                    _userName = value;
                    this.OnPropertyChanged("UserName");
                }

            }
        }

        private string _password="password";

        public string Password
        {
            get { return _password; }
            set
            {
                if (_password != value)
                {
                    _password = value;
                    this.OnPropertyChanged("Password");
                }

            }
        }



        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

    public class TopicModel: INotifyPropertyChanged
    {
        public TopicModel()
        {

        }
        public TopicModel(string topic,string describe)
        {
            _isSelected = false;
            _topic = topic;
            _describe = describe;
        }
        private bool? _isSelected;

        public bool? IsSelected
        {
            get { return _isSelected; }
            set
            {
                if (_isSelected!=value)
                {
                    _isSelected = value;
                    OnPropertyChanged("IsSelected");
                }
            }
        }

        private string _topic;


        public string Topic
        {
            get { return _topic; }
            set
            {
                if (_topic!=value)
                {
                    _topic = value;
                    OnPropertyChanged("Topic");
                }
            }
        }

        private string _describe;

        public string Describe
        {
            get { return _describe; }
            set
            {
                if (_describe!=value)
                {
                    _describe = value;
                    OnPropertyChanged("Describe");
                }
            }
        }


        public event PropertyChangedEventHandler PropertyChanged;
        protected virtual void OnPropertyChanged(string propertyName = null)
        {
            var handler = PropertyChanged;
            if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
        }
    }

}

  


源码:

https://github.com/ludewig/MqttDemo
鸣谢:

https://blog.csdn.net/xiakexingtx/article/details/108281359

https://blog.csdn.net/lordwish/article/details/84970800?spm=1001.2101.3001.6661.1&utm_medium=distribute.pc_relevant_t0.none-task-download-2%7Edefault%7ECTRLIST%7EPaid-1-13762029-blog-84970800.pc_relevant_paycolumn_v3&depth_1-utm_source=distribute.pc_relevant_t0.none-task-download-2%7Edefault%7ECTRLIST%7EPaid-1-13762029-blog-84970800.pc_relevant_paycolumn_v3&utm_relevant_index=1

posted @ 2022-06-29 16:14  春光牛牛  阅读(7067)  评论(0编辑  收藏  举报