MQTT nodejs中搭建mqtt 几个MQTT的知识点

import mqtt from 'mqtt'

import MQTTConnect from '../../assets/js/MQTTConnect'

created: function () {
  this.userAgent = localStorage.getItem('devUserAgent')
  this.loadConnect()  ------>加载默认配置信息
},
computed: {
  getStatus () {
    if (this.client.connected) {
      console.log('已连接')
      return '已连接'
    }
    if (this.loading) {
      return '正在连接...'
    }
    return '连接已断开'
  },
  supportWebSocket () {
    return window.WebSocket
  }
},
mounted: function () {
  this.initWVBridge()
  this.getWebVersion()
  this.getUserInfo()
  this.getDevInfo()
  this.getLocation()
  this.formateDateTime = this.formatDate(this.creatTime)
  this.mqttConnect()------->客户端接收消息
  var self = this
   setTimeout(function () {
    self.subTopic = '/xxx/' + self.xwNum
    self.mqttSubscribe()------客户端接收订阅信息
  }, 500)

  //localStorage.storageWebVersion = '2.1'

  // /iot/fise/in/Car
  // {"type": 1,"data": {"lng": "113.989822","lat": "22.681385","from_type": 1}}
  // from_type = 0-GPS定位 1-基站定位 2-WIFI定位
  // {"type": 3,"data": {"battery":100,"mileage":1388,"defend":false,"work":true,"speed":300,"voltage":421,"status":{"control_error":false,"switch_error":false,"brake_error":false,"system_error":true}}}
},
getWebVersion () {
  var self = this
  webVersion({
    app_key: localStorage.storageAppKey,
    app_dev: localStorage.storageAppDev
  }).then(function (res) {
    if (res.data.error_code === 0) {
      self.newVersion = res.data.h5_version;
      if (localStorage.storageWebVersion) {
          //判断当前页面是否是新版本,如果是新版本清理webview缓存
          //console.log(localStorage.storageWebVersion + ',' + self.newVersion)
          if (localStorage.storageWebVersion != self.newVersion) {
            //self.logInfo = '发现新版本:' + localStorage.storageWebVersion + ',' + self.newVersion
            if (self.userAgent == 'android') {
              //调用安卓函数清除webview缓存数据
              window.android.jsCall_CleanCache()
              localStorage.storageWebVersion = self.newVersion;
            } else if (self.userAgent == 'ios') {
              //调用ios函数清除webview缓存数据
              connectWebViewJavascriptBridge(function (bridge) {
                bridge.callHandler('jsCall_cleanWebViewCache', {}, function (response) {
                })
              })
              localStorage.storageWebVersion = self.newVersion;
            }
          } else {
            //self.logInfo = '已是最新版本:' + localStorage.storageWebVersion + ',' + self.newVersion
            localStorage.storageWebVersion = self.newVersion;
          }
      } else {
        localStorage.storageWebVersion = self.newVersion;
      }
    }
  }).catch(function (error) {
    if (error.message === 'Network Error') {
      Toast({
        message: '网络异常',
        duration: 2000
      })
    }
  })
},
initWVBridge () {
  if (this.userAgent === 'ios') {
    // 联系xxx
    document.getElementById('contactDevBtn2').onclick = function () {
      connectWebViewJavascriptBridge(function (bridge) {
        bridge.callHandler('jsCall_ContactDev', {}, function (response) {
        })
      })
    }
    // 返回Appxxx页面
    document.getElementById('jsCallGoBack2').onclick = function () {
      connectWebViewJavascriptBridge(function (bridge) {
        bridge.callHandler('jsCall_go_back', {}, function (response) {
        })
      })
      this.mqttDisconnect()
    }
  } else if (this.userAgent === 'android') {
    // 联系xxx
    document.getElementById('contactDevBtn2').onclick = function () {
      window.android.jsCall_ContactDev()
    }
    // 返回App xxx页面
    document.getElementById('jsCallGoBack2').onclick = function () {
      window.android.jsCall_go_back()
      this.mqttDisconnect()
    }
  }
},
// mqtt Websocket
disconnectSwitch () {
  // connecting
  if (this.loading && !this.client.connected) {
    this.loading = false
    this.client.end()
    this.client = {}
  } else {
    this.mqttDisconnect()
  }
},
mqttConnect () {
  if (!this.supportWebSocket) {
    console.log('不支持webSocket')
    return
  }
  // 防止通过键盘事件连接
  if (this.client.connected || this.loading) {
    return
  }
  this.loading = true
  this.retryTimes = 0
  const options = {-------->加上鉴权auth信息,否则知道IP就可以connect mqtt不安全
    keepalive: this.keepalive,
    username: this.username,
    password: this.password,
    clientId: this.clientId,
    clean: this.clean,
    connectTimeout: 4000
  }
  const protocol = this.isSSL ? 'wss' : 'ws'
  this.client = mqtt.connect(`${protocol}://${this.host}:${this.port}/mqtt`, options)--------->var client2 = mqtt.connect("mqtt://192.168.1.136:8000");
  this.client.on('connect', () => {
    this.loading = false
  })
  this.client.on('reconnect', () => {
    if (this.retryTimes > 1) {
      if (this.sending) {
        console.log('连接错误')
      } else {
        console.log(`${'连接失败'} ${this.host}:${this.port}`)
      }
      this.retryTimes = 0
      this.sending = false
      this.loading = false
      this.client.end()
      this.client = {}
    }
    // 发送不合法的主题触发
    if (this.sending) {
      console.log('连接错误')
    }
    this.retryTimes += 1
  })
  this.client.on('error', (error) => {
    console.log(error)
    // 防止重复连接
    this.retryTimes = 0
  })
  // --------
  // 接收消息
  this.client.on('message', (topic, message, packet) => {---------->client2.on('message',function(top,message) {  console.log(message.toString());  }); 
    /* this.receivedMessages.unshift({
      topic,
      message: message.toString()
    }) */
    console.log('信息已接收')
    var msgInfo = JSON.parse(message)
    this.receivedMessages = msgInfo
    console.log(msgInfo)
    var self = this
    if (msgInfo.type === 1) {
      self.setMap(msgInfo.data.lng, msgInfo.data.lat)
    } else if (msgInfo.type === 2) {
      self.electricizeStatus = msgInfo.data.event_key
    } else if (msgInfo.type === 3) {
      self.batteryVal = msgInfo.data.battery
      self.totleMileage = msgInfo.data.mileage
      self.preventStatus = msgInfo.data.defend
      self.startStatus = msgInfo.data.work
      self.voltageVal = msgInfo.data.voltage / 10
      self.speedVal = msgInfo.data.speed / 10
      localStorage.storageCarErrorInfo = JSON.stringify(msgInfo.data.status)
    }
  })
},
mqttDisconnect () {
  if (this.client.connected) {
    this.client.end()
    this.client.on('close', () => {
      this.loading = false
      this.reset()
      this.client = {}
    })
  } else {
    console.log('连接已断开')
  }
},
mqttSubscribe () {
  if (this.client.connected) {
     this.subscriptions.forEach((x, index) => {
        if (x.topic === this.subTopic) {
          this.subscriptions.splice(index, index + 1)
        }
     })
     this.client.subscribe(this.subTopic, (error) => {------> client2.subscribe('test',{qos:1});//订阅主题为test的消息
        if (error) {
          console.log(error.toString())
        } else {
          this.subscriptions.unshift({
            topic: this.subTopic
          })
          console.log('订阅成功')
        }
     })
  } else {
     console.log('连接已断开')
  }
},
mqttCacheScuscribe (topic) {
  if (!this.client.connected) {
    console.log('连接已断开')
    return
  }
  this.client.unsubscribe(topic, (error) => {
    if (error) {
      console.log('取消订阅失败')
      return
    }
    this.subscriptions.forEach((element, index) => {
      if (element.topic === topic) {
        this.subscriptions.splice(index, 1)
        // clear message which in this topic
      }
    })
  })
},
  reset () {
    this.subscriptions = []
    this.receivedMessages = []
    this.subTopic = '/xxx/' + this.xwNum
  },
  loadConnect () {
    console.log(MQTTConnect.options)
    if (MQTTConnect.client && MQTTConnect.client.connected) {
      this.client = MQTTConnect.client
      Object.keys(MQTTConnect.options).forEach((item) => {
        this[item] = MQTTConnect.options[item]
      })
    }
  },
  stashConnect () {
    MQTTConnect.client = this.client
    console.log(MQTTConnect.options)
    Object.keys(MQTTConnect.options).forEach((item) => {
      MQTTConnect.options[item] = this[item]
    })
  }
}


MQTTConnect.js:

export default {
  client: {},
  options: {
    host: '',
    port: 8083,
    username: '',
    isSSL: false,
    password: '',
    keepalive: 60,
    clean: true,
    clientId: '',
    subQos: 0,
    publishQos: 0,
    publishMessage: 'Hello world!',
    subTopic: '/World',
    publishTopic: '/World',
    publishRetain: false,
    receivedMessages: [],
    publishedMessages: [],
    subscriptions: [],
  },
}



https://blog.csdn.net/swimming_in_it_/article/details/78017624

nodejs中搭建mqtt

使用nodejs编写一个简单的服务器,然后在编写两个客户端,一个客户端用来接收消息,另一个客户端用来发送消息:

扫描二维码关注公众号,回复: 1913508 查看本文章

服务器代码为:不过,你第一次运行的时候,会缺少mosca模块,自己npm install mosca即可。具体功能,学过编程的大致都能看懂,然后里面都有注释。

[javascript]  view plain  copy
  1. var mosca = require('mosca');//构建服务器  
  2. /*var settings = { 
  3.     port:8989, 
  4.     backend:{ 
  5.         type:'zmq', 
  6.         json:false, 
  7.         zmq:require("zmq"), 
  8.         port:"tcp://127.0.0.1:33334", 
  9.         controlPort:"tcp://127.0.0.1:33334", 
  10.         delay:5 
  11.     }, 
  12.     persistence: { 
  13.         factory:mosca.persistence.Mongo, 
  14.         url:"mongodb://localhost:270117/mosca" 
  15.     } 
  16. }; 
  17. var MqttServer = new mosca.Server(settings);*/  
  18. var MqttServer = new mosca.Server({  
  19.     port:8000  
  20. });  
  21.   
  22. //监听链接  
  23. MqttServer.on("clientConnected",function(client) {  
  24.     console.log("client connected",client.id);  
  25. });  
  26. //监听mqtt的主题消息  
  27. MqttServer.on("published",function(packet,client) {//当客户端有连接的时候,发布主题消息  
  28.     var topic = packet.topic;  
  29.     console.log(packet);  
  30.     switch(topic) {  
  31.         case 'test':  
  32.             console.log("message-publish",packet.payload.toString());  
  33.             //mqtt转发主题消息  
  34.             MqttServer.publish({topic:'other',payload:'这是服务端!'});  
  35.             break;  
  36.         case 'test2':  
  37.             console.log("payload:",packet.payload.toString());  
  38.             var msg = {  
  39.                 topic:'repeat',  
  40.                 payload:packet.payload,  
  41.                 qos:0,  
  42.                 retain:false  
  43.             };  
  44.             MqttServer.publish(msg,function() {  
  45.                 console.log('repeat!   ');  
  46.             });  
  47.             break;  
  48.         case 'other':  
  49.             console.log("case-other:",packet.payload.toString());  
  50.             break;  
  51.     }  
  52. });  
  53. MqttServer.on('ready',function() {  
  54.     console.log("mqtt is running....");  
  55. });  

发送消息客户端:

[javascript]  view plain  copy
  1. var mqtt = require('mqtt');  
  2. var client = mqtt.connect('mqtt://192.168.1.136:8000');//连接到服务端  
  3. var num = 0;  
  4. var qtt = {};//定义消息(可以为字符串,对象等)  
  5. qtt.aa = '发布';  
  6. qtt.bb = '消息!';  
  7.   
  8. setInterval(function() {  
  9.     //发布主题为Test的消息  
  10.     client.publish('test',JSON.stringify(qtt),{qos:1,retain:true});//hello mqtt + num++  
  11. },1000);  
接收消息的客户端;

[javascript]  view plain  copy
  1. var mqtt = require('mqtt');  
  2. var client2 = mqtt.connect("mqtt://192.168.1.136:8000");  
  3.   
  4. client2.subscribe('test',{qos:1});//订阅主题为test的消息  
  5.   
  6. client2.on('message',function(top,message) {  
  7.     console.log(message.toString());  
  8. });  
至此第一个简单的nodejs版的mqtt的demo已经搭建完毕,后期具体的开发,见后文的博客!



https://blog.csdn.net/u011216417/article/details/69666752

基本概念 Basic Conception
Session 会话
定义
  • 定义:某个客户端(由ClientID作为标识)和某个服务器之间的逻辑层面的通信
  • 生命周期(存在时间):会话 >= 网络连接
ClientID
  • 客户端唯一标识,服务端用于关联一个Session
  • 只能包含这些 大写字母,小写字母 和 数字(0-9a-zA-Z),23个字符以内
  • 如果 ClientID 在多次 TCP连接中保持一致,客户端和服务器端会保留会话信息(Session)
  • 同一时间内 Server 和同一个 ClientID 只能保持一个 TCP 连接,再次连接会踢掉前一个
CleanSession 标记
  • 在Connect时,由客户端设置 
  • 0 —— 开启会话重用机制。网络断开重连后,恢复之前的Session信息。需要客户端和服务器有相关Session持久化机制。
  • 1 —— 关闭会话重用机制。每次Connect都是一个新Session,会话仅持续和网络连接同样长的时间。
客户端 Session
  • 已经发送给服务端,但是还没有完成确认的 QoS 1 和 QoS 2 级别的消息 
  • 已从服务端接收,但是还没有完成确认的 QoS 2 级别的消息
服务器端 Session
  • 会话是否存在,即使会话状态的其它部分都是空  (SessionFlag)
  • 客户端的订阅信息  (ClientSubcription)
  • 已经发送给客户端,但是还没有完成确认的 QoS 1 和 QoS 2 级别的消息
  • 即将传输给客户端的 QoS 1 和 QoS 2 级别的消息
  • 已从客户端接收,但是还没有完成确认的 QoS 2 级别的消息
  • (可选)准备发送给客户端的 QoS 0 级别的消息
长连接维护与管理
Keep Alive 心跳
  • 目的是保持长连接的可靠性,以及双方对彼此是否在线的确认。
  • 客户端在Connect的时候设置 Keep Alive 时长。如果服务端在 1.5 * KeepAlive 时间内没有收到客户端的报文,它必须断开客户端的网络连接
  • Keep Alive 的值由具体应用指定,一般是几分钟。允许的最大值是 18 小时 12 分 15 秒
Will 遗嘱
  • 遗嘱消息(Will Message)存储在服务端,当网络连接关闭时,服务端必须发布这个遗嘱消息,所以被形象地称之为遗嘱,可用于通知异常断线。
  • 客户端发送 DISCONNECT 关闭链接,遗嘱失效并删除
  • 遗嘱消息发布的条件,包括: 
  • 服务端检测到了一个 I/O 错误或者网络故障
  • 客户端在保持连接(Keep Alive)的时间内未能通讯
  • 客户端没有先发送 DISCONNECT 报文直接关闭了网络连接
  • 由于协议错误服务端关闭了网络连接
  • 相关设置项,需要在Connect时,由客户端指定
  • Will Flag —— 遗嘱的总开关
  • 0 -- 关闭遗嘱功能,Will QoS 和 Will Retain 必须为 0
  • 1 --  开启遗嘱功能,需要设置 Will Retain 和 Will QoS
  • Will QoS —— 遗嘱消息 QoS
  • 可取值 0、1、2,含义与消息QoS相同
  • Will Retain —— 遗嘱是否保留
  • 0 -- 遗嘱消息不保留,后面再订阅不会收到消息
  • 1 -- 遗嘱消息保留,持久存储
  • Will Topic —— 遗嘱话题
  • Will Payload —— 遗嘱消息内容
消息基本概念
报文标识 Packet  Identifier                                                      
  • 存在报文的可变报头部分,非零两个字节整数 (0-65535]
  • 一个流程中重复:这些报文包含 PacketID,而且在一次通信流程内保持一致:
  • PUBLISH(QoS>0 时)PUBACKPUBRECPUBRELPUBCOMP
  • SUBSCRIBE,  SUBACK
  • UNSUBSCIBEUNSUBACK                                        
  • 新的不重复:客户端每次发送一个新的这些类型的报文时都必须分配一个当前 未使用的PacketID
  • 当客户端处理完这个报文对应的确认后,这个报文标识符就释放可重用。
  • 独立维护:客户端和服务端彼此独立地分配报文标识符。因此,客户端服务端组合使用相同的报文标识符可以实现 并发 的消息交换。可能出现一下情况,并不算异常:
Payload 有效载荷,消息体
  • 最大允许 256MB
  • Publish 的 Payload 允许为空。在很多场合下,代表将持久消息(或者遗嘱消息)清空。
  • UTF-8编码
Retain 持久消息(粘性消息)
  • RETAIN 标记:每个Publish消息都需要指定的标记
  • 0 —— 服务端不能存储这个消息也不能移除或替换任何 现存的保留消息               
  • 1 —— 服务端必须存储这个应用消息和它的QoS等级,以便它可以被分发给未来的订阅者 
  • 每个Topic只会保留最多一个 Retain 持久消息
  • 客户端订阅带有持久消息的Topic,会立即受到这条消息
  • 服务器可以选择丢弃持久消息,比如内存或者存储吃紧的时候
  • 如果客户端想要删除某个Topic 上面的持久消息,可以向这个Topic发送一个Payload为空的持久消息
  • 遗嘱消息(Will)的Retain持久机制同理
QoS 服务等级(消息可靠性)
最多一次 At most Once( QoS == 0)
  • 没有回复,不需要存储。有可能丢失(网络异常断开,业务层繁忙或者错误)
至少一次 At least Once( QoS == 1 
  • 发送者S 发送前需要做持久化存储,接受者R 不需要持久化存储
  • 如果 发送者S 没有收到 接收者R 的回复 PUBACK,过一段时间 发送者S 会重新发送,DUP标记为1(在同一Session内)。
  • 接受者R 发送 PUBACK 后,不需要知道对方是否收到,马上把消息交给上层业务。如果此时网络异常,会导致发送者重发。这样接受者收到多个消息(所以叫至少一次)。
有且仅有一次 Exactly Once( QoS == 2 )
  • 发送者S 发送 PUBLISH 前,需要做持久化存储。接受者R 回复PUBREC 后,也需要做持久化存储
  • 如果 发送者S 没有收到 接收者R 的回复 PUBREC,过一段时间 发送者S 会重新发送,DUP标记为1(在同一Session内)。
  • 如果 接受者R 没有收到 发送者S 的回复 PUBREL,过一段时间 接受者R 会重新发送PUBREC。
  • 发送者S 收到 PUBREC后,删除持久化消息,但是要保存 PacketID
  • 接收者R 受到 PUBREL后,删除持久化PUBREC。然后将消息发给上层,同时回复 PUBCOMP。
  • 发送者S 收到 PUBCOMP 后,删除 PacketID,通信完美结束。
  • 这套流程可以 严格保证 一个包不管在什么情况下 接收者R 只收到一次 。
重传标记 DUP 与重传机制 (QoS > 0)
  • 如果客户端或者服务器发送了一个 Publish 消息,一段时间内没收到 PublishAck 回复,则认为消息丢失,进行重传。
  • 在一个Session内,进行重传的时候,头部的 DUP 重传标志 设置为1。
  • 客户端有可能收到 DUP == 0 的重传包(Payload相同,PacketID不同)。因为可能因为网络问题,下次重传时间较久,Session已经释放,PacketID 已经变更。
  • 客户端发给服务器的和服务器转发给别的客户端的 Publish 消息,DUP 重传标志不会传递
  • 接收者收到一个 DUP 标志为 1 的控制报文时,并不能保证之前收到过相同的报文
消息 重传顺序                                    
  • 重发任何之前的 PUBLISH 报文时,必须按原始 PUBLISH 报文的发送顺序重发 (适用于QoS 1 和 QoS 2 消息)
  • 必须按照对应的 PUBLISH 报文的顺序发送 PUBACK 报文 (QoS 1 消息)
  • 必须按照对应的 PUBLISH 报文的顺序发送 PUBREC 报文 (QoS 2 消息)
  • 必须按照对应的 PUBREC 报文的顺序发送 PUBREL 报文 (QoS 2 消息)
  • QoS == 1 时,虽然是PUBLISH有序的,但是可能会重复。例如,发布者按顺序 1,2,3,4 发送消息,订阅者收到的顺序可能是 1,2,3,2,3,4。 
  • QoS == 1 时,如果限制 传输窗口 (in-flight window==1,即同一时刻只有一个包在传输,就可以保证乱序。例如,订阅者收到的顺序可能是 1,2,3,3,4,而不是 1,2,3,2,3,4 
  • QoS == 2 时,肯定不会存在乱序的问题。

话题 与订阅机制   Topic & Subcribe
Topic 话题 和  TopicFilter 话题过滤器
  • Pub-Sub消息模型的核心机制
  • UTF-8 编码字符串,不能超过 65535 字节。层级数量没有限制
  • 不能包含任何的下文中提到的特殊符号(/、+、#),必须至少包含一个字符  
  • 区分大小写,可以包含空格不能包含空字符 (Unicode U+0000)  
  • 在收部或尾部增加 斜杠 “/”,会产生不同的Topic和TopicFilter。举例:
  • “/A” 和 A” 是不同的
  • “A” 和 “A/” 是不同的
  • 只包含斜杠 “/”  Topic  TopicFilter 是合法的                                                           
TopicFilter 中的特殊符号
  • 层级分隔符 /
  • 用于分割主题的每个层级,为主题名提供一个分层结构       
  • 主题层级分隔符可以出现在 Topic 或 TopicFilter 的任何位置                           
  • 特例:相邻的主题层次分隔符表示一个零长度的主题层级         
  • 单层通配符 +
  • 只能用于单个主题层级匹配的通配符。例如,“a/b/+” 匹配 “a/b/c1” 和 “a/b/c2” ,但是不匹配 “a/b/c/d”      
  • 可以匹配 任意层级,包括第一个和最后一个层级。例如,“+” 是有效的“sport/+/player1” 也是有效的。
  • 可以在多个层级中使用它,也可以和多层通配符一起使用。 例如,“+/tennis/#” 是有效的。                
  • 只能匹配本级不能匹配上级。例如,“sport/+” 不匹配 “sport” 但是却匹配“sport/”“/finance” 匹配 “+/+” 和 “/+” ,但是不匹配 “+”。 
  • 多层通配符 #
  • 用于匹配主题中任意层级的通配符
  • 匹配包含本身的层级和子层级。例如 a/b/c/#" 可以匹配 “a/b/c”、a/b/c/d 和  a/b/c/d/e
  • 必须是最后的结尾。例如“sport/tennis/#/ranking”是无效的      
  • “#”是有效的,会收到所有的应用消息。 (服务器端应将此类 TopicFilter禁掉 )
$ 开头的 ,服务器保留
  • 服务端不能将 $ 字符开头的 Topic 匹配通配符 (#或+) 开头的 TopicFilter
  • 服务端应该阻止客户端使用这种 Topic 与其它客户端交换消息。服务端实现可以将 $ 开头的主题名用作其他目的。
  • $SYS/ 被广泛用作包含服务器特定信息或控制接口的主题的前缀
  • 客户端不特意订阅 $开头的 Topic,就不会收到对应的消息
  • 订阅 “#” 的客户端不会收到任何发布到以 “$” 开头主题的消息
  • 订阅 “+/A/B” 的客户端不会收到任何发布到 “$SYS/A/B” 的消息
  • 订阅 “$SYS/#” 的客户端会收到发布到以 “$SYS/” 开头主题的消息
  • 订阅 “$SYS/A/+” 的客户端会收到发布到 “$SYS/A/B” 主题的消息
  • 如果客户端想同时接受以 “$SYS/” 开头主题的消息和不以 $ 开头主题的消息,它需要同时 订阅 “#” 和 “$SYS/#”
订阅  Subscribe   与 QoS降级
  • 订阅机制基于TopicFilter匹配
  • 一个Subsribe请求 可订阅多个 Topic(节省带宽,多订阅尽量用一次请求)。取消订阅也同理
  • 每一个订阅需要指定一个QoS,指定了客户端接收消息所允许的最大QoS级别。但是服务器端最终授权返回的QoS可能会小于等于客户端请求的QoS
  • 对于高于QoS的消息(比如说订阅的QoS限制到1,消息的QoS指定到2),那么客户端会收到一个QoS降低为指定的 限制QoS 的消息(消息的QoS降为1,不保证只收到一次)
  • 订阅关系可以被覆盖,以TopicFilter为标识。如果后面订阅一个相同的TopicFilter,但是指定的QoS不同,则以后面的为准,QoS升高后,重发相应等级的 Retain 消息
安全传输与 鉴权认证  S ecurity & Certification
传输层
  • 可以采用 TCP、SSL/TLS [RFC5246WebSocket 作为传输层。UDP不可以,因为不保证可靠传输与有序传输。
  • 服务器端返回的数据极有可能出现 粘包 的情况。客户端经常会在连接建立之后,连续调用多个订阅,这样服务器端就会回复多个订阅ACK包,同时还有各个Topic上的持久消息,一般粘成一个TCP包返回过来
  • 端口(IANA分发)
  • 1883:over TCP,无加密
  • 8883:over SSL/TLS,单向认证(强烈建议)
  • 8884:over SSL/TLS,双向认证
  • 8080:over WebSockets,未加密
  • 8081:over WebSockets,加密                                                  
  • 可使用SOCKS代理,可利用安全隧道(如SSH)
潜在的风险与应对机制
  • 潜在风险
  • 设备可能会被盗用
  • 客户端和服务端的静态数据可以被访问(比如客户端Root导致数据泄露、服务器被拖库)
  • 协议规定的行为可能有副作用 (如计时器攻击  “timing attacks”)
  • 拒绝服务攻击(DoS)
  • 通信可能会被拦截、修改、重定向或者泄露(抓包、中间人)
  • 虚假控制报文注入
  • 应对的机制
  • 用户和设备身份认证
  • 服务端资源访问授权
  • 控制报文和 Payload 的完整性校验
  • 控制报文和 Payload 的隐私控制
客户端身份验证 与授权   ( Authentication & Authorization of Client
  • 用户名+密码验证:Connect 登录的时候,传入 UserName 和 Password
  • 相关标记位:在Connect时,由客户端设置
  • 用户名(UserName Flag)标记设置为1,才可以穿入
  • 密码(Password Flag)标记设置为1
  • 外部验证:LDAP、OAuth 或者 操作系统的认证机制
  • 用户名密码加密:防止中间人攻击和重放攻击
  • 应用层:客户端通过应用消息给服务端发送凭证用于身份验证。
  • 授权:基于客户端提供的信息如用户名、客户端标识符(ClientId)、客户端的主机名或 IP 地址,或者身份认证的结果,服务端可以限制对某些服务端资源的访问
服务端身份验证 ( Authentication of  Server  by Client
  • MQTT 协议不是双向信任的,它没有提供客户端验证服务端身份的机制
  • TLS:客户端可以使用服务端发送的SSL证书验证服务端的身份
  • 应用层:可以通过服务端给客户端发送凭证用于身份验证的应用层消息
  • VPN:在客户端和服务端之间使用虚拟专用网(VPN)可以确保客户端连接的是预期的服务器。
控制报文和 Payload 的完整性(Integrity)
  • TLS:提供了对网络传输的数据做完整性校验的哈希算法
  • 应用层:可以在应用消息中单独包含哈希值。这样做可以为 PUBLISH 控制报文的网络传输和静态数据提供内容的完整性检查
  • VPN:在客户端和服务端之间使用虚拟专用网(VPN)连接可以在 VPN 覆盖的网络段提供数据完整性检查
控制报文和  Payload  的保密性 (Privacy)
  • 轻量级加密:AES or DES,可适用于低端设备
  • TLS:可以对网络传输的数据加密
  • 应用层:可以单独加密 Payload 内容。这可以提供 Payload 传输途中和静态数据的私密性。但不能给应用消息的其它属性如 Topic 加密
  • 静态数据加密:客户端和服务端实现可以加密存储静态数据,例如可以将应用消息作为会话的一部分存储
  • VPN:在客户端和服务端之间使用虚拟专用网(VPN)连接可以在 VPN 覆盖的网络段保证数据的私密性
异常行为的检测
  • 服务端实现可以监视客户端的行为,检测潜在的安全风险。例如:
  • 重复的连接请求
  • 重复的身份验证请求
  • 连接的异常终止
  • 主题扫描 (请求发送或订阅大量主题)
  • 发送无法送达的消息 (没有订阅者的主题)   
  • 客户端连接但是不发送数据
  • 应对策略
  • 发现违反安全规则的行为,服务端实现可以断开客户端连接
  • 可以基于 IP地址 或 ClientID 实现一个 动态黑名单列表
  • 可以使用网络层面的控制,实现基于 IP 地址或其它信息的 速率限制 或黑名单
  • 连接拒绝与错误码



https://blog.csdn.net/wangshuminjava/article/details/80578109

几个MQTT的知识点


开始正文前需要感谢一下网友“小龙”和emqtt.io群里的网友们的帮助,本人刚刚开始使用MQTT有很多不懂的地方,在emqtt.io群里询问解决方法的时候,“小龙”给我详细的讲解了一些MQTT的知识点并提供了解决方案,甚是感谢。本人觉得里面提到的一些东西对初学者还是很有用的,因此,在这里把我与“小龙”的聊天记录整理一下供大家参考。

问题一:单片机缓存有限,处理能力有限的情况下,消息不可能一次发出,这种情况下要怎样通过MQTT发布消息?

先组装publish协议的头,里面写好payload的长度,通过tcp发出去,然后一点一点发payload。如果拿不到payload总长度,这个事情就难办。因为你发了一个publish协议报,服务端读到head里面的payload的长度之后,就会一直读,读到需要的长度,才算这个publish结束。所以,你的需求是首先确认你发布的内容的长度,然后组publish数据包头,填写payload长度,tcp:send(head),之后一段一段发送payload,比如一次1k,还是按照正常的tcp发送数据,一直发送完毕,就算这个publish结束了。接下来的send,都属于tcp层的,这些不用你干预的。如果在tcp层,send失败,那一定是socket有问题了,连接断了,你就得重新连接mqtt服务器了,如果没有发完,那么服务端的会话也会结束,就是说服务端也没有接收到数据。重新连接,你就得重发,发送消息中途,只要断掉,就得重连,是否要重发数据,取决于你是否保存了以前的数据。还有如果消息重要,可以使用qos=1或者2,可以确保服务端收到消息,qos=1需要一个来回,qos=2需要四个来回,qos=0非常简单,只要send出去就不管了。

问题二:MQTT的开源资源多吗?

http://www.mqtt.org里面很多

问题三:MQTT为什么一般不提供持久化的功能

mqtt协议里面是按照设备一直在线设计的,数据都是保存在内存里的

问题四:MQTT是不是很耗内存?

MQTT是比较吃内存的,emqtt的实测数据是:38W,内存占14G,CPU 15%

问题五:session与client之间的关系是怎样的?

这样的,比如你一个板子,作为客户端,发起mqtt的连接请求connect到mqtt服务器,比如说就是emqtt服务吧,emqtt服务端收到这个板子的连接请求之后,在tcp层上会和板子建立一个tcp的连接,在emqtt内部,会产生一个进程,和这个板子做数据通讯,同时还会产生一个进程,叫session,这个sessoin是专门管理这个板子订阅的主题,其它板子如果发布了这个板子感兴趣的主题的时候,也会发到这个板子对应的这个session里面,如果这个session收到订阅的主题之后,发现对用的client还活着,就通过这个client把数据经过tcp发到这个板子上,如果发现client已经没有了,就是说板子和服务端断掉了,那么session就会把收到的订阅的主题,先保存在session里面,下次板子连接上了,而且cleansession=false,那么这个session就不会清除,在这次连接时,就会把以前收到的订阅消息,发给板子,大概就是这个意思。

问题六:emqtt怎么知道连上来的client是同一个呢?

连接的时候,需要设置一个clientid,这个id可以不设置,如果不设置,在emqtt服务端会自动产生一个唯一的id,如果你要用到session,必须有一个唯一个id,你可以用imei。如果你一定要收到离线消息的话,就必须使用确定的id了。

问题七:emqtt的session时间可以修改吗?

你可以改一下session的时间,现在是48小时,你可以改为一周,如果要永久,恐怕emqtt不太适合。

问题八:emqtt的访问权限什么的都是写在配置文件里的?

etc/acl.config

问题九:emqtt的分布式是怎么回事?

分布式简单说就是把你的几台服务器连在一起,里面任意一台或者多台,只要不是全坏了,emqtt都可以正常运行。emqtt的数据是在几个节点共享的,某个节点出问题,数据是不会丢的,但是节点上的session数据会丢失。



使用nodejs编写一个简单的服务器,然后在编写两个客户端,一个客户端用来接收消息,另一个客户端用来发送消息:

服务器代码为:不过,你第一次运行的时候,会缺少mosca模块,自己npm install mosca即可。具体功能,学过编程的大致都能看懂,然后里面都有注释。

[javascript]  view plain  copy
  1. var mosca = require('mosca');//构建服务器  
  2. /*var settings = { 
  3.     port:8989, 
  4.     backend:{ 
  5.         type:'zmq', 
  6.         json:false, 
  7.         zmq:require("zmq"), 
  8.         port:"tcp://127.0.0.1:33334", 
  9.         controlPort:"tcp://127.0.0.1:33334", 
  10.         delay:5 
  11.     }, 
  12.     persistence: { 
  13.         factory:mosca.persistence.Mongo, 
  14.         url:"mongodb://localhost:270117/mosca" 
  15.     } 
  16. }; 
  17. var MqttServer = new mosca.Server(settings);*/  
  18. var MqttServer = new mosca.Server({  
  19.     port:8000  
  20. });  
  21.   
  22. //监听链接  
  23. MqttServer.on("clientConnected",function(client) {  
  24.     console.log("client connected",client.id);  
  25. });  
  26. //监听mqtt的主题消息  
  27. MqttServer.on("published",function(packet,client) {//当客户端有连接的时候,发布主题消息  
  28.     var topic = packet.topic;  
  29.     console.log(packet);  
  30.     switch(topic) {  
  31.         case 'test':  
  32.             console.log("message-publish",packet.payload.toString());  
  33.             //mqtt转发主题消息  
  34.             MqttServer.publish({topic:'other',payload:'这是服务端!'});  
  35.             break;  
  36.         case 'test2':  
  37.             console.log("payload:",packet.payload.toString());  
  38.             var msg = {  
  39.                 topic:'repeat',  
  40.                 payload:packet.payload,  
  41.                 qos:0,  
  42.                 retain:false  
  43.             };  
  44.             MqttServer.publish(msg,function() {  
  45.                 console.log('repeat!   ');  
  46.             });  
  47.             break;  
  48.         case 'other':  
  49.             console.log("case-other:",packet.payload.toString());  
  50.             break;  
  51.     }  
  52. });  
  53. MqttServer.on('ready',function() {  
  54.     console.log("mqtt is running....");  
  55. });  

发送消息客户端:

[javascript]  view plain  copy
  1. var mqtt = require('mqtt');  
  2. var client = mqtt.connect('mqtt://192.168.1.136:8000');//连接到服务端  
  3. var num = 0;  
  4. var qtt = {};//定义消息(可以为字符串,对象等)  
  5. qtt.aa = '发布';  
  6. qtt.bb = '消息!';  
  7.   
  8. setInterval(function() {  
  9.     //发布主题为Test的消息  
  10.     client.publish('test',JSON.stringify(qtt),{qos:1,retain:true});//hello mqtt + num++  
  11. },1000);  
接收消息的客户端;

[javascript]  view plain  copy
  1. var mqtt = require('mqtt');  
  2. var client2 = mqtt.connect("mqtt://192.168.1.136:8000");  
  3.   
  4. client2.subscribe('test',{qos:1});//订阅主题为test的消息  
  5.   
  6. client2.on('message',function(top,message) {  
  7.     console.log(message.toString());  
  8. });  
至此第一个简单的nodejs版的mqtt的demo已经搭建完毕,后期具体的开发,见后文的博客!

猜你喜欢

转载自blog.csdn.net/zgpeterliu/article/details/80803931