mqtt.lua 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510
  1. --- 模块功能:MQTT客户端
  2. -- @module mqtt
  3. -- @author openLuat
  4. -- @license MIT
  5. -- @copyright openLuat
  6. -- @release 2017.10.24
  7. require "log"
  8. require "socket"
  9. require "utils"
  10. module(..., package.seeall)
  11. -- MQTT 指令id
  12. local CONNECT, CONNACK, PUBLISH, PUBACK, PUBREC, PUBREL, PUBCOMP, SUBSCRIBE, SUBACK, UNSUBSCRIBE, UNSUBACK, PINGREQ, PINGRESP, DISCONNECT = 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14
  13. local CLIENT_COMMAND_TIMEOUT = 60000
  14. local function encodeLen(len)
  15. local s = ""
  16. local digit
  17. repeat
  18. digit = len % 128
  19. len = (len - digit) / 128
  20. if len > 0 then
  21. digit = bit.bor(digit, 0x80)
  22. end
  23. s = s .. string.char(digit)
  24. until (len <= 0)
  25. return s
  26. end
  27. local function encodeUTF8(s)
  28. if not s or #s == 0 then
  29. return ""
  30. else
  31. return pack.pack(">P", s)
  32. end
  33. end
  34. local function packCONNECT(clientId, keepAlive, username, password, cleanSession, will, version)
  35. local content = pack.pack(">PbbHPAAAA",
  36. version == "3.1" and "MQIsdp" or "MQTT",
  37. version == "3.1" and 3 or 4,
  38. (#username == 0 and 0 or 1) * 128 + (#password == 0 and 0 or 1) * 64 + will.retain * 32 + will.qos * 8 + will.flag * 4 + cleanSession * 2,
  39. keepAlive,
  40. clientId,
  41. encodeUTF8(will.topic),
  42. encodeUTF8(will.payload),
  43. encodeUTF8(username),
  44. encodeUTF8(password))
  45. return pack.pack(">bAA",
  46. CONNECT * 16,
  47. encodeLen(string.len(content)),
  48. content)
  49. end
  50. local function packSUBSCRIBE(dup, packetId, topics)
  51. local header = SUBSCRIBE * 16 + dup * 8 + 2
  52. local data = pack.pack(">H", packetId)
  53. for topic, qos in pairs(topics) do
  54. data = data .. pack.pack(">Pb", topic, qos)
  55. end
  56. return pack.pack(">bAA", header, encodeLen(#data), data)
  57. end
  58. local function packUNSUBSCRIBE(dup, packetId, topics)
  59. local header = UNSUBSCRIBE * 16 + dup * 8 + 2
  60. local data = pack.pack(">H", packetId)
  61. for k, topic in pairs(topics) do
  62. data = data .. pack.pack(">P", topic)
  63. end
  64. return pack.pack(">bAA", header, encodeLen(#data), data)
  65. end
  66. local function packPUBLISH(dup, qos, retain, packetId, topic, payload)
  67. local header = PUBLISH * 16 + dup * 8 + qos * 2 + retain
  68. local len = 2 + #topic + #payload
  69. if qos > 0 then
  70. return pack.pack(">bAPHA", header, encodeLen(len + 2), topic, packetId, payload)
  71. else
  72. return pack.pack(">bAPA", header, encodeLen(len), topic, payload)
  73. end
  74. end
  75. local function packACK(id, dup, packetId)
  76. return pack.pack(">bbH", id * 16 + dup * 8 + (id == PUBREL and 1 or 0) * 2, 0x02, packetId)
  77. end
  78. local function packZeroData(id, dup, qos, retain)
  79. dup = dup or 0
  80. qos = qos or 0
  81. retain = retain or 0
  82. return pack.pack(">bb", id * 16 + dup * 8 + qos * 2 + retain, 0)
  83. end
  84. local function unpack(s)
  85. if #s < 2 then return end
  86. log.debug("mqtt.unpack", #s, string.toHex(string.sub(s, 1, 50)))
  87. -- read remaining length
  88. local len = 0
  89. local multiplier = 1
  90. local pos = 2
  91. repeat
  92. if pos > #s then return end
  93. local digit = string.byte(s, pos)
  94. len = len + ((digit % 128) * multiplier)
  95. multiplier = multiplier * 128
  96. pos = pos + 1
  97. until digit < 128
  98. if #s < len + pos - 1 then return end
  99. local header = string.byte(s, 1)
  100. local packet = {id = (header - (header % 16)) / 16, dup = ((header % 16) - ((header % 16) % 8)) / 8, qos = bit.band(header, 0x06) / 2, retain = bit.band(header, 0x01)}
  101. local nextpos
  102. if packet.id == CONNACK then
  103. nextpos, packet.ackFlag, packet.rc = pack.unpack(s, "bb", pos)
  104. elseif packet.id == SUBACK then
  105. nextpos, packet.ackFlag, packet.rc = pack.unpack(s, "bb", pos)
  106. if len >= 2 then
  107. nextpos, packet.packetId = pack.unpack(s, ">H", pos)
  108. packet.grantedQos = string.sub(s, nextpos, pos + len - 1)
  109. else
  110. packet.packetId = 0
  111. packet.grantedQos = ""
  112. end
  113. elseif packet.id == PUBLISH then
  114. nextpos, packet.topic = pack.unpack(s, ">P", pos)
  115. if packet.qos > 0 then
  116. nextpos, packet.packetId = pack.unpack(s, ">H", nextpos)
  117. end
  118. packet.payload = string.sub(s, nextpos, pos + len - 1)
  119. elseif packet.id ~= PINGRESP then
  120. if len >= 2 then
  121. nextpos, packet.packetId = pack.unpack(s, ">H", pos)
  122. else
  123. packet.packetId = 0
  124. end
  125. end
  126. return packet, pos + len
  127. end
  128. local mqttc = {}
  129. mqttc.__index = mqttc
  130. --- 创建一个mqtt client实例
  131. -- @string clientId 确保设备唯一性
  132. -- @number[opt=300] keepAlive 心跳间隔(单位为秒),默认300秒
  133. -- @string[opt=""] username 用户名,用户名为空配置为""或者nil
  134. -- @string[opt=""] password 密码,密码为空配置为""或者nil
  135. -- @number[opt=1] cleanSession 1/0
  136. -- @table[opt=nil] will 遗嘱参数,格式为{qos=,retain=,topic=,payload=}
  137. -- @string[opt="3.1.1"] version MQTT版本号,仅支持"3.1"和"3.1.1"
  138. -- @return table mqttc client实例
  139. -- @usage
  140. -- mqttc = mqtt.client("clientid-123")
  141. -- mqttc = mqtt.client("clientid-123",200)
  142. -- mqttc = mqtt.client("clientid-123",nil,"user","password")
  143. -- mqttc = mqtt.client("clientid-123",nil,"user","password",nil,{qos=0,retain=0,topic="willTopic",payload="willTopic"},"3.1")
  144. function client(clientId, keepAlive, username, password, cleanSession, will, version)
  145. local o = {}
  146. local packetId = 1
  147. if will then
  148. will.flag = 1
  149. else
  150. will = {flag = 0, qos = 0, retain = 0, topic = "", payload = ""}
  151. end
  152. o.clientId = clientId
  153. o.keepAlive = keepAlive or 300
  154. o.username = username or ""
  155. o.password = password or ""
  156. o.cleanSession = cleanSession or 1
  157. o.version = version or "3.1.1"
  158. o.will = will
  159. o.commandTimeout = CLIENT_COMMAND_TIMEOUT
  160. o.cache = {}-- 接收到的mqtt数据包缓冲
  161. o.inbuf = "" -- 未完成的数据缓冲
  162. o.connected = false
  163. o.getNextPacketId = function()
  164. packetId = packetId == 65535 and 1 or (packetId + 1)
  165. return packetId
  166. end
  167. o.lastOTime = 0
  168. setmetatable(o, mqttc)
  169. return o
  170. end
  171. -- 检测是否需要发送心跳包
  172. function mqttc:checkKeepAlive()
  173. if self.keepAlive == 0 then return true end
  174. if os.time() - self.lastOTime >= self.keepAlive then
  175. if not self:write(packZeroData(PINGREQ)) then
  176. log.info("mqtt.client:", "pingreq send fail")
  177. return false
  178. end
  179. end
  180. return true
  181. end
  182. -- 发送mqtt数据
  183. function mqttc:write(data)
  184. log.debug("mqtt.client:write", string.toHex(string.sub(data, 1, 50)))
  185. local r = self.io:send(data)
  186. if r then self.lastOTime = os.time() end
  187. return r
  188. end
  189. -- 接收mqtt数据包
  190. function mqttc:read(timeout, msg, msgNoResume)
  191. if not self:checkKeepAlive() then
  192. log.warn("mqtt.read checkKeepAlive fail")
  193. return false
  194. end
  195. -- 处理之前缓冲的数据
  196. local packet, nextpos = unpack(self.inbuf)
  197. if packet then
  198. self.inbuf = string.sub(self.inbuf, nextpos)
  199. return true, packet
  200. end
  201. while true do
  202. local recvTimeout
  203. if self.keepAlive == 0 then
  204. recvTimeout = timeout
  205. else
  206. local kaTimeout = (self.keepAlive - (os.time() - self.lastOTime)) * 1000
  207. recvTimeout = kaTimeout > timeout and timeout or kaTimeout
  208. end
  209. local r, s, p = self.io:recv(recvTimeout == 0 and 5 or recvTimeout, msg, msgNoResume)
  210. if r then
  211. self.inbuf = self.inbuf .. s
  212. elseif s == "timeout" then -- 超时,判断是否需要发送心跳包
  213. if not self:checkKeepAlive() then
  214. return false
  215. elseif timeout <= recvTimeout then
  216. return false, "timeout"
  217. else
  218. timeout = timeout - recvTimeout
  219. end
  220. else -- 其他错误直接返回
  221. return r, s, p
  222. end
  223. local packet, nextpos = unpack(self.inbuf)
  224. if packet then
  225. --self.lastIOTime = os.time()
  226. self.inbuf = string.sub(self.inbuf, nextpos)
  227. if packet.id ~= PINGRESP then
  228. return true, packet
  229. end
  230. end
  231. end
  232. end
  233. -- 等待接收指定的mqtt消息
  234. function mqttc:waitfor(id, timeout, msg, msgNoResume)
  235. for index, packet in ipairs(self.cache) do
  236. if packet.id == id then
  237. return true, table.remove(self.cache, index)
  238. end
  239. end
  240. while true do
  241. local insertCache = true
  242. local r, data, param = self:read(timeout, msg, msgNoResume)
  243. if r then
  244. if data.id == PUBLISH then
  245. if data.qos > 0 then
  246. if not self:write(packACK(data.qos == 1 and PUBACK or PUBREC, 0, data.packetId)) then
  247. log.info("mqtt.client:waitfor", "send publish ack failed", data.qos)
  248. return false
  249. end
  250. end
  251. elseif data.id == PUBREC or data.id == PUBREL then
  252. if not self:write(packACK(data.id == PUBREC and PUBREL or PUBCOMP, 0, data.packetId)) then
  253. log.info("mqtt.client:waitfor", "send ack fail", data.id == PUBREC and "PUBREC" or "PUBCOMP")
  254. return false
  255. end
  256. insertCache = false
  257. end
  258. if data.id == id then
  259. return true, data
  260. end
  261. if insertCache then table.insert(self.cache, data) end
  262. else
  263. return false, data, param
  264. end
  265. end
  266. end
  267. --- 连接mqtt服务器
  268. -- @string host 服务器地址
  269. -- @param port string或者number类型,服务器端口
  270. -- @string[opt="tcp"] transport "tcp"或者"tcp_ssl"
  271. -- @table[opt=nil] cert table或者nil类型,ssl证书,当transport为"tcp_ssl"时,此参数才有意义。cert格式如下:
  272. -- {
  273. -- caCert = "ca.crt", --CA证书文件(Base64编码 X.509格式),如果存在此参数,则表示客户端会对服务器的证书进行校验;不存在则不校验
  274. -- clientCert = "client.crt", --客户端证书文件(Base64编码 X.509格式),服务器对客户端的证书进行校验时会用到此参数
  275. -- clientKey = "client.key", --客户端私钥文件(Base64编码 X.509格式)
  276. -- clientPassword = "123456", --客户端证书文件密码[可选]
  277. -- }
  278. -- @number[opt=120] timeout 可选参数,socket连接超时时间,单位秒
  279. -- @return result true表示成功,false或者nil表示失败
  280. -- @usage mqttc = mqtt.client("clientid-123", nil, nil, false); mqttc:connect("mqttserver.com", 1883, "tcp", 5)
  281. function mqttc:connect(host, port, transport, cert, timeout)
  282. if self.connected then
  283. log.info("mqtt.client:connect", "has connected")
  284. return false
  285. end
  286. if self.io then
  287. self.io:close()
  288. self.io = nil
  289. end
  290. if transport and transport ~= "tcp" and transport ~= "tcp_ssl" then
  291. log.info("mqtt.client:connect", "invalid transport", transport)
  292. return false
  293. end
  294. self.io = socket.tcp(transport == "tcp_ssl" or type(cert) == "table", cert)
  295. if not self.io:connect(host, port, timeout) then
  296. log.info("mqtt.client:connect", "connect host fail")
  297. return false
  298. end
  299. if not self:write(packCONNECT(self.clientId, self.keepAlive, self.username, self.password, self.cleanSession, self.will, self.version)) then
  300. log.info("mqtt.client:connect", "send fail")
  301. return false
  302. end
  303. local r, packet = self:waitfor(CONNACK, self.commandTimeout, nil, true)
  304. -- if not r or packet.rc ~= 0 then
  305. -- log.info("mqtt.client:connect", "connack error", r and packet.rc or -1)
  306. -- return false,packet.rc
  307. -- end
  308. if (not r) or (not packet) or packet.rc ~= 0 then
  309. log.info("mqtt.client:connect", "connack error", r and packet.rc or -1)
  310. return false, packet and packet.rc or -1
  311. end
  312. self.connected = true
  313. return true
  314. end
  315. --- 订阅主题
  316. -- @param topic string或者table类型,一个主题时为string类型,多个主题时为table类型,主题内容为UTF8编码
  317. -- @param[opt=0] qos number或者nil,topic为一个主题时,qos为number类型(0/1/2,默认0);topic为多个主题时,qos为nil
  318. -- @return bool true表示成功,false或者nil表示失败
  319. -- @usage
  320. -- mqttc:subscribe("/abc", 0) -- subscribe topic "/abc" with qos = 0
  321. -- mqttc:subscribe({["/topic1"] = 0, ["/topic2"] = 1, ["/topic3"] = 2}) -- subscribe multi topic
  322. function mqttc:subscribe(topic, qos)
  323. if not self.connected then
  324. log.info("mqtt.client:subscribe", "not connected")
  325. return false
  326. end
  327. local topics
  328. if type(topic) == "string" then
  329. topics = {[topic] = qos and qos or 0}
  330. else
  331. topics = topic
  332. end
  333. if not self:write(packSUBSCRIBE(0, self.getNextPacketId(), topics)) then
  334. log.info("mqtt.client:subscribe", "send failed")
  335. return false
  336. end
  337. local r, packet = self:waitfor(SUBACK, self.commandTimeout, nil, true)
  338. if not r then
  339. log.info("mqtt.client:subscribe", "wait ack failed")
  340. return false
  341. end
  342. if not (packet.grantedQos and packet.grantedQos~="" and not packet.grantedQos:match(string.char(0x80))) then
  343. log.info("mqtt.client:subscribe", "suback grant qos error", packet.grantedQos)
  344. return false
  345. end
  346. return true
  347. end
  348. --- 取消订阅主题
  349. -- @param topic string或者table类型,一个主题时为string类型,多个主题时为table类型,主题内容为UTF8编码
  350. -- @return bool true表示成功,false或者nil表示失败
  351. -- @usage
  352. -- mqttc:unsubscribe("/abc") -- unsubscribe topic "/abc"
  353. -- mqttc:unsubscribe({"/topic1", "/topic2", "/topic3"}) -- unsubscribe multi topic
  354. function mqttc:unsubscribe(topic)
  355. if not self.connected then
  356. log.info("mqtt.client:unsubscribe", "not connected")
  357. return false
  358. end
  359. local topics
  360. if type(topic) == "string" then
  361. topics = {topic}
  362. else
  363. topics = topic
  364. end
  365. if not self:write(packUNSUBSCRIBE(0, self.getNextPacketId(), topics)) then
  366. log.info("mqtt.client:unsubscribe", "send failed")
  367. return false
  368. end
  369. if not self:waitfor(UNSUBACK, self.commandTimeout, nil, true) then
  370. log.info("mqtt.client:unsubscribe", "wait ack failed")
  371. return false
  372. end
  373. return true
  374. end
  375. --- 发布一条消息
  376. -- @string topic UTF8编码的字符串
  377. -- @string payload 用户自己控制payload的编码,mqtt.lua不会对payload做任何编码转换
  378. -- @number[opt=0] qos 0/1/2, default 0
  379. -- @number[opt=0] retain 0或者1
  380. -- @return bool 发布成功返回true,失败返回false
  381. -- @usage
  382. -- mqttc = mqtt.client("clientid-123", nil, nil, false)
  383. -- mqttc:connect("mqttserver.com", 1883, "tcp")
  384. -- mqttc:publish("/topic", "publish from luat mqtt client", 0)
  385. function mqttc:publish(topic, payload, qos, retain)
  386. if not self.connected then
  387. log.info("mqtt.client:publish", "not connected")
  388. return false
  389. end
  390. qos = qos or 0
  391. retain = retain or 0
  392. if not self:write(packPUBLISH(0, qos, retain, qos > 0 and self.getNextPacketId() or 0, topic, payload)) then
  393. log.info("mqtt.client:publish", "socket send failed")
  394. return false
  395. end
  396. if qos == 0 then return true end
  397. if not self:waitfor(qos == 1 and PUBACK or PUBCOMP, self.commandTimeout, nil, true) then
  398. log.warn("mqtt.client:publish", "wait ack timeout")
  399. return false
  400. end
  401. return true
  402. end
  403. --- 接收消息
  404. -- @number timeout 接收超时时间,单位毫秒
  405. -- @string[opt=nil] msg 可选参数,控制socket所在的线程退出recv阻塞状态
  406. -- @return result 数据接收结果,true表示成功,false表示失败
  407. -- @return data
  408. -- 如果result为true,表示服务器发过来的mqtt包
  409. --
  410. -- 如果result为false,超时失败,data为"timeout"
  411. -- 如果result为false,msg控制退出,data为msg的字符串
  412. -- 如果result为false,socket连接被动断开控制退出,data为"CLOSED"
  413. -- 如果result为false,PDP断开连接控制退出,data为"IP_ERROR_IND"
  414. --
  415. -- 如果result为false,mqtt不处于连接状态,data为nil
  416. -- 如果result为false,收到了PUBLISH报文,发送PUBACK或者PUBREC报文失败,data为nil
  417. -- 如果result为false,收到了PUBREC报文,发送PUBREL报文失败,data为nil
  418. -- 如果result为false,收到了PUBREL报文,发送PUBCOMP报文失败,data为nil
  419. -- 如果result为false,发送PINGREQ报文失败,data为nil
  420. -- @return param 如果是msg控制退出,param的值是msg的参数;其余情况无意义,为nil
  421. -- @usage
  422. -- true, packet = mqttc:receive(2000)
  423. -- false, error_message = mqttc:receive(2000)
  424. -- false, msg, para = mqttc:receive(2000,"APP_SEND_DATA")
  425. function mqttc:receive(timeout, msg)
  426. if not self.connected then
  427. log.info("mqtt.client:receive", "not connected")
  428. return false
  429. end
  430. return self:waitfor(PUBLISH, timeout, msg)
  431. end
  432. --- 断开与服务器的连接
  433. -- @return nil
  434. -- @usage
  435. -- mqttc = mqtt.client("clientid-123", nil, nil, false)
  436. -- mqttc:connect("mqttserver.com", 1883, "tcp")
  437. -- process data
  438. -- mqttc:disconnect()
  439. function mqttc:disconnect()
  440. if self.io then
  441. if self.connected then self:write(packZeroData(DISCONNECT)) end
  442. self.io:close()
  443. self.io = nil
  444. end
  445. self.cache = {}
  446. self.inbuf = ""
  447. self.connected = false
  448. end