记录一次编写C#MQTT服务器监听客户端在线与离线的过程

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第1天,点击查看活动详情

一个艰深的解决历程。


周二晚上开会的时候,老大给出了需求,要求我可以写一个实时显示(基于MQTT)这个用户名下所有客户端和它的在线或离线状态的功能。

我脑瓜子一转,这特么不简单吗! 虽然我只是个大三实习生,但是这个也难不倒我好吧!

02AC6990020EC0249D496F7AF86BAEDD.gif

获取这个用户名下的客户端,只要写个查询数据库的代码就可以解决了!

实时显示不也简单吗!我只要知道它在不在线来更改状态就行了……就行了……行了……了。

但是我的程序怎么知道它在不在线呢

2269c6361bef40c215ea539c86fd705.jpg

最开始的想法

但是,我脑瓜子灵机一动,就有了个“绝妙”的思路!(我甚至画了个思维导图)

image.png

拿表里面的客户端一个个去连服务器,然后如果连不上就是证明有人已经用了,在线!如果连得上就是没人用,就是不在线!对吧对吧!这逻辑没毛病。

我按照这个逻辑和思路去编写了功能,但是现实很快就打了我的脸。结果是,无论如何都是连接成功,what,我整个人都疑惑了。难道我的思路有问题?

掘金(1).png

MQTT协议:给你个大比兜!

在这里给大家科普一下MQTT协议。

MQTT的小科普


MQTT协议的全称是Message Queuing Telemetry Transport,中文名叫作消息队列遥测传输。是支持所有平台的一个即时通讯协议,该协议可以当作传感器来使用。

它几乎可以把所有联网物品和外部连接起来,底层基于TCP,使用发布/订阅消息模式,提供一对多的消息发布,解除应用程序耦合……

而当MQTT协议连接设备时候,不同设备使用相同的client ID连接服务器,clientID作为设备的专属识别符,需为全局唯一

那么如果不同的设备使用相同的clientID同时连接MQTT服务器——————

那么先连接的那个设备会被强制断开……

先连接的那个设备会被强制断开……

那个设备会被强制断开……

掘金(2).png 草(一种植物)所我无论怎么连都能连上,因为我把前面先连的给踹了!真流氓啊!我喜欢。


新的思路

但是我并不死心,我重新开始了头脑风暴。

不久,我就又想到了"绝妙的思路",我哈哈大笑,我笑诸葛少智,周瑜无谋!……咳咳。跑题了。

掘金(3).png

如果每个客户端都订阅了用户的主题,上线和断线后都往用户那里发一句"XXXX(这里放ClientID)已启动/断开"。

public void ConnectedHandler(IManagedMqttClient mqttClient,RichTextBox M)//当客户端连接时
 {
   if(mqttClient.Options.ClientOptions.ClientId==ClientID_Read)//假如接收到的连接信息是本客户端的才提示
    {
       if (M.InvokeRequired)
        {
          M.Invoke(new EventHandler(delegate {
             M.AppendText(">>>服务器已连接!\n");
                    }));
         }
     }
   string Msg = mqttClient.Options.ClientOptions.ClientId + "在线";//客户端ID以及状态
   topic = ClientForm.MiniTopicName;//获取用户主题名
   mqttClient.PublishAsync(topic, Msg);//发送给用户
            
}
复制代码

然后用户那里再获取到这些东西,将其与表中的ClientID做对比,再改变其在线状态。

public void ToReceived(MqttApplicationMessageReceivedEventArgs e)//连接或断开消息接收
{
   if (e.ApplicationMessage.Topic == ClientForm.CurrentName)//假如收到订阅了该用户的客户端消息

   {

     string msg = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);//获取消息

     string ClientID = msg.Remove(ClientID_Read.Length-1);//只留下客户端ID

     string StateMsg = msg.Remove(0, ClientID_Read.Length-1);//留下启动或者断开的消息

     NewStateShow(ClientID, StateMsg, dataGridView,ClientForm.CurrentName);//刷新在线列表

    }
            
}
复制代码

刷新的方法。

public void ToFresh(string ClientId,string stateTxt, DataGridView dataGridView, string Name)//刷新最新的在线状态

{
   //获取列表的ClientId
   List<string> GridViewClientIdlist = new List<string>();
   //string DataGridViewClientId = "";
   //循环获取并判断

   for (int i = 0; i < dataGridView.RowCount; i++)

   {
      GridViewClientIdlist.Add(dataGridView.Rows[i].Cells[0].Value.ToString());//获取到ClientID所在cell的值
    }
   if (GridViewClientIdlist.IndexOf(ClientId)>-1)//查找List中是否有ClientID,如果有则执行

   {
       string str = $"update PortServerTable set State='{stateTxt}' where ClientID='{ClientId}'";//改变状态语句
        SqlCommand cmd = new SqlCommand(str, DB.cn);
        cmd.ExecuteNonQuery();//执行语句
         if (dataGridView.InvokeRequired)//跨线程访问
        {
             dataGridView.Invoke(new EventHandler(delegate
            {
                ds.Clear();//清空临时表
                string str1 = $"select ClientID,State from PortServerTable where UserName='{Name}'";//只查询客户端id和状态
                SqlShowPortServer = new SqlDataAdapter(str1, DB.cn);
                SqlShowPortServer.Fill(ds, "PortInfo");
                DataView ClientPortshow = new DataView(ds.Tables["PortInfo"]);//更新
                dataGridView.DataSource = ClientPortshow;//填充数据

                    }));

                }

            }

        }

复制代码

这招如何!简直天衣无缝!

我得意洋洋地测试,一切也正如我所料,只要上线下线了就会刷新在线的状态。但是很快我发现了不对劲。

不对,只要我用了两个以上的客户端进行连接或者是我频繁地连接或者断开

刷新就不管用了!这怎么回事!!!这不对!!明明……逻辑就是这样的!!(流泪)

我进行了断点测试,发现每当多次访问DataGridView的线程后,就会突然有一瞬间不会去执行invoke里的方法,就不会刷新了。(这里至今搞不明白,如果有大佬明白,请指导我

掘金(4).png

最后,我突然想到。

MQTT会不会有内置的功能可以调用呢

不会吧,不会吧。那我岂不是小丑了。写了老半天,其实人家有正确答案我不抄??

我急忙再次看了MQTT协议。果然,是有的。 其实这个在MQTT协议中已经给出系统主题,MQTT服务端可以知道客户端的任何情况,比如:什么时候上线和下线。

下面是Topic的写法,你只要将这下面两句或者最后一句写入你的MQTT服务器的高级订阅中,当客户端连接服务器时即可获得相应信息。

$SYS/brokers/${node}/clients/${clientid}/connected:上线事件。某客户端上线时,会向该主题(Topic)发布消息

 $SYS/brokers/${node}/clients/${clientid}/disconnected:掉线事件。某客户端掉线时,会向该主题(Topic)发布消息

$SYS/brokers/+/clients/#:通用事件。某客户端发布,订阅,上下线时都会向该主题(Topic)发送消息。

复制代码

这次又是对协议不熟,已经不懂服务器方面配置的锅,小丑竟是我自己

掘金(5).png

总算是有个可靠的获取客户端上下线的方法了!经过测试,也是稳稳地收到了。

但是由于发送过来的是Json格式的消息,程序需要反序列化才能获取到有用的信息。这里就有必要引入一个非常方便的类库去操作。


开源的类库Newtonsoft.Json

适用于 .NET 的流行高性能 JSON 框架

image.png 直接在项目中反手一个安装引用即可获得强大的JSON处理功能!


接下来就是获取跟处理。

string res = Encoding.UTF8.GetString(e.ApplicationMessage.Payload);//获取到服务器发送的消息
var pStr = JsonConvert.DeserializeObject<Client>(res);//引用的反序列化方法与自己定义的类做处理
if (IsMine(pStr.clientid, dataGridView))//判断是否是用户的客户端,与DataView做对比
{
  if (e.ApplicationMessage.Topic.EndsWith("disconnected"))//假如离线
      ToFresh(pStr.clientid, false);//上面写过的刷新方法,进行了改写,假如离线返回false
  else if(e.ApplicationMessage.Topic.EndsWith("connected"))
      ToFresh(pStr.clientid, true);
 }
复制代码

简单,又优雅,完美解决问题。我之前想的那些思想精华显得我很傻


监听在线与离线功能编写的总结

  1. 不要使用相同的ClientID去进行连接,即使相同,也会踢出前面一个,没有任何意义。
  2. 多次订阅相同主题进行发送,返回太多信息,会给线程巨大压力程序容易崩溃,即使成功实现,也意义不大。而且,当客户端发送其他消息时,程序无法辨别是上线或者下线的信息而也去加以循环判断做比较,一下子就会出异常
  3. 请使用MQTT内置的主题订阅

掘金(6).png

好了,以上就是全部内容,只是个人的一点开发时的记录,希望对大家有些帮助或者启发。希望下次不会这么困难了。当然我知道,没有最困难,只有更困难。

但是勇敢的程序员,开摆不怕困难!!!

猜你喜欢

转载自juejin.im/post/7128418282297622536