mqttClient.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392
  1. package mqttClient
  2. import (
  3. "crypto/tls"
  4. "errors"
  5. "fmt"
  6. "log"
  7. "net/url"
  8. "os"
  9. "reflect"
  10. "sync"
  11. "time"
  12. "unsafe"
  13. mqtt "github.com/eclipse/paho.mqtt.golang"
  14. "go.uber.org/zap"
  15. )
  16. type MqttClientOpt struct {
  17. Broker string // 服务端地址
  18. ClientID string // (可选) 客户端名称
  19. TLS *tls.Config
  20. }
  21. type MqttClient struct {
  22. client mqtt.Client
  23. clientOptions *mqtt.ClientOptions
  24. subscribeMap map[string]func(payload []byte)
  25. subscribeMu sync.Mutex // 保护 subscribeMap(OnConnect 回调与调用方在不同 goroutine)
  26. clientID string // 客户端名称
  27. }
  28. const (
  29. connectTimeout = 30 * time.Second // 30s
  30. reconnectTimeout = 10 * time.Second // 10s
  31. disconnectTimeout = 10 * 1000 // 10s
  32. subscribeMsgTimeout = 30 * time.Second // 30s
  33. sendMsgTimeout = 30 * time.Second // 30s
  34. )
  35. // 创建客户端
  36. func CreateMqttClient(opt MqttClientOpt) (*MqttClient, error) {
  37. // 初始化前设置(放在 InitFwhMqttClient 开头或 init() 中)
  38. mqtt.DEBUG = log.New(os.Stdout, "[paho-DEBUG] ", log.Ltime|log.Lmicroseconds)
  39. mqtt.WARN = log.New(os.Stdout, "[paho-WARN] ", log.Ltime|log.Lmicroseconds)
  40. mqtt.ERROR = log.New(os.Stderr, "[paho-ERROR] ", log.Ltime|log.Lmicroseconds)
  41. // 创建客户端选项
  42. opts := mqtt.NewClientOptions()
  43. opts.AddBroker(opt.Broker)
  44. clientID := "go_mqtt_client"
  45. if len(opt.ClientID) > 0 {
  46. clientID = opt.ClientID
  47. }
  48. opts.SetClientID(clientID)
  49. opts.SetAutoReconnect(true) // 自动重连
  50. opts.SetCleanSession(true) // 不保留会话
  51. opts.SetTLSConfig(opt.TLS)
  52. mqttClient := MqttClient{
  53. subscribeMap: map[string]func(payload []byte){},
  54. clientID: clientID,
  55. }
  56. // 连接成功回调
  57. opts.OnConnect = func(c mqtt.Client) {
  58. zap.L().Info("Connected to MQTT broker", zap.String("clientID", clientID))
  59. // 重新订阅丢失的 Subscribe
  60. mqttClient.ResumeSubscribe()
  61. }
  62. // 连接丢失回调
  63. opts.OnConnectionLost = func(c mqtt.Client, err error) {
  64. fmt.Printf("%s Connection lost: %s\n", clientID, err.Error())
  65. zap.L().Info("MQTT Connection lost", zap.String("clientID", clientID), zap.String("err", err.Error()))
  66. }
  67. // 重连回调
  68. opts.OnReconnecting = func(c mqtt.Client, opts *mqtt.ClientOptions) {
  69. zap.L().Info("MQTT Reconnecting", zap.String("clientID", clientID))
  70. }
  71. // 尝试连接回调
  72. opts.OnConnectAttempt = func(broker *url.URL, tlsCfg *tls.Config) *tls.Config {
  73. zap.L().Info("MQTT Attempt Connect", zap.String("clientID", clientID), zap.String("Broker", broker.String()))
  74. return tlsCfg
  75. }
  76. // 连接事件回调
  77. opts.OnConnectionNotification = func(c mqtt.Client, cn mqtt.ConnectionNotification) {
  78. zap.L().Info("MQTT Connecting Notification", zap.String("clientID", clientID), zap.Int64("Notification", int64(cn.Type())))
  79. }
  80. // 创建客户端
  81. mqttClient.client = mqtt.NewClient(opts)
  82. mqttClient.clientOptions = opts
  83. return &mqttClient, nil
  84. }
  85. func (c *MqttClient) RecreateClient() error {
  86. if c.clientOptions == nil {
  87. return errors.New("RecreateClient failed, ClientOptions is nil.")
  88. }
  89. // 尝试断开旧连接(非阻塞)
  90. oldClient := c.client
  91. go func() {
  92. if oldClient != nil {
  93. done := make(chan struct{})
  94. go func() {
  95. oldClient.Disconnect(disconnectTimeout)
  96. close(done)
  97. }()
  98. select {
  99. case <-done:
  100. case <-time.After(15 * time.Second):
  101. zap.L().Warn("[MQTT] old client disconnect timeout, abandoning")
  102. }
  103. }
  104. }()
  105. // 重新创建客户端
  106. c.client = mqtt.NewClient(c.clientOptions)
  107. zap.L().Info("[MQTT] create new client")
  108. return nil
  109. }
  110. func (c *MqttClient) Connect() error {
  111. isConnected := c.client.IsConnected()
  112. isOpen := c.client.IsConnectionOpen()
  113. zap.L().Debug("[MQTT] Connect() status check",
  114. zap.Bool("IsConnected", isConnected),
  115. zap.Bool("IsConnectionOpen", isOpen),
  116. )
  117. // 底层连接已活跃,无需再连
  118. if isOpen {
  119. return nil
  120. }
  121. // IsConnected=true 但连接不活跃 → 正在重连中,等待恢复
  122. if isConnected && !isOpen {
  123. zap.L().Info("[MQTT] client is reconnecting, waiting for connection to restore")
  124. deadline := time.Now().Add(reconnectTimeout)
  125. for time.Now().Before(deadline) {
  126. if c.client.IsConnectionOpen() {
  127. return nil
  128. }
  129. // 如果 IsConnected 也变成 false 了,说明重连失败/放弃了
  130. if !c.client.IsConnected() {
  131. zap.L().Warn("[MQTT] client lost connection during reconnect wait, will re-connect")
  132. break
  133. }
  134. time.Sleep(500 * time.Millisecond)
  135. }
  136. // 等待超时或重连失败,检查最终状态
  137. if c.client.IsConnectionOpen() {
  138. return nil
  139. }
  140. // 强制断开后重连(清除 disconnecting 卡死状态)
  141. // zap.L().Warn("[MQTT] reconnect failed, forcing disconnect and re-connect")
  142. // c.client.Disconnect(0)
  143. // time.Sleep(100 * time.Millisecond)
  144. }
  145. // 真正断开状态,执行连接
  146. token := c.client.Connect()
  147. if !token.WaitTimeout(connectTimeout) {
  148. if err := c.RecreateClient(); err != nil {
  149. return fmt.Errorf("recreate client failed (%s)", c.clientID)
  150. }
  151. token = c.client.Connect()
  152. if !token.WaitTimeout(connectTimeout) {
  153. return fmt.Errorf("connect timeout (%s)", c.clientID)
  154. }
  155. }
  156. return token.Error()
  157. }
  158. // 断开连接
  159. func (c *MqttClient) Disconnect() {
  160. // 清空订阅
  161. c.CleanSubscribe()
  162. // 安全断开,等待最多 10 秒让队列消息发送完
  163. c.client.Disconnect(disconnectTimeout)
  164. }
  165. // 检查是否连接
  166. func (c *MqttClient) IsConnected() bool {
  167. return c.client.IsConnected()
  168. }
  169. // 恢复订阅消息
  170. func (c *MqttClient) ResumeSubscribe() {
  171. c.subscribeMu.Lock()
  172. subs := make(map[string]func(payload []byte), len(c.subscribeMap))
  173. for k, v := range c.subscribeMap {
  174. subs[k] = v
  175. }
  176. c.subscribeMu.Unlock()
  177. for k, v := range subs {
  178. if err := c.subscribeMessage(k, v); err != nil {
  179. zap.L().Error("[MQTT] resume subscribe failed", zap.String("topic", k), zap.String("err", err.Error()))
  180. }
  181. }
  182. }
  183. // 清空所有订阅
  184. func (c *MqttClient) CleanSubscribe() {
  185. c.subscribeMu.Lock()
  186. defer c.subscribeMu.Unlock()
  187. for k := range c.subscribeMap {
  188. delete(c.subscribeMap, k)
  189. }
  190. }
  191. // 订阅消息
  192. func (c *MqttClient) subscribeMessage(topic string, callback func(payload []byte)) error {
  193. if !c.client.IsConnected() {
  194. return fmt.Errorf("subscribe failed. (client not connected)")
  195. }
  196. token := c.client.Subscribe(topic, 1, func(c mqtt.Client, m mqtt.Message) {
  197. callback(m.Payload())
  198. })
  199. if !token.WaitTimeout(subscribeMsgTimeout) {
  200. return fmt.Errorf("subscribe timeout (topic:%s)", topic)
  201. }
  202. err := token.Error()
  203. if err != nil {
  204. return err
  205. }
  206. return nil
  207. }
  208. // 订阅消息
  209. func (c *MqttClient) SubscribeMessage(topic string, callback func(payload []byte)) error {
  210. err := c.subscribeMessage(topic, callback)
  211. if err != nil {
  212. return err
  213. }
  214. // 备份subscribe
  215. c.subscribeMu.Lock()
  216. c.subscribeMap[topic] = callback
  217. c.subscribeMu.Unlock()
  218. return nil
  219. }
  220. // 取消订阅
  221. func (c *MqttClient) UnSubscribeMessage(topic string) error {
  222. if !c.client.IsConnected() {
  223. return fmt.Errorf("unsubscribe failed. (client not connected)")
  224. }
  225. token := c.client.Unsubscribe(topic)
  226. if !token.WaitTimeout(subscribeMsgTimeout) {
  227. return fmt.Errorf("unsubscribe timeout (topic:%s)", topic)
  228. }
  229. err := token.Error()
  230. if err != nil {
  231. return err
  232. }
  233. c.subscribeMu.Lock()
  234. delete(c.subscribeMap, topic)
  235. c.subscribeMu.Unlock()
  236. return nil
  237. }
  238. // 发送消息,只尝试一次,不管是否送达
  239. func (c *MqttClient) SendMessageOnce(topic string, msg []byte) error {
  240. if !c.client.IsConnected() {
  241. return fmt.Errorf("send message once failed. (client not connected)")
  242. }
  243. token := c.client.Publish(topic, 0, false, msg)
  244. if !token.WaitTimeout(sendMsgTimeout) {
  245. return fmt.Errorf("send message once timeout (topic:%s, msg:%s)", topic, msg)
  246. }
  247. return token.Error()
  248. }
  249. // 发送消息,没收到 ACK 就重发,至少送到一次,可能收到多次
  250. func (c *MqttClient) SendMessage(topic string, msg []byte, retained bool) error {
  251. if !c.client.IsConnected() {
  252. return fmt.Errorf("send message failed. (client not connected)")
  253. }
  254. token := c.client.Publish(topic, 1, retained, msg)
  255. if !token.WaitTimeout(sendMsgTimeout) {
  256. return fmt.Errorf("send message timeout (topic:%s, msg:%s)", topic, msg)
  257. }
  258. return token.Error()
  259. }
  260. // 发送消息,严格一次,不重复,不丢失
  261. func (c *MqttClient) SendMessageMustOnce(topic string, msg []byte, retained bool) error {
  262. if !c.client.IsConnected() {
  263. return fmt.Errorf("send message must once failed. (client not connected)")
  264. }
  265. token := c.client.Publish(topic, 2, retained, msg)
  266. if !token.WaitTimeout(sendMsgTimeout) {
  267. return fmt.Errorf("send message must once timeout (topic:%s, msg:%s)", topic, msg)
  268. }
  269. return token.Error()
  270. }
  271. // 发送消息,没收到 ACK 就重发,至少送到一次,可能收到多次,并保留消息在Broker,新定义的设备会收到消息
  272. func (c *MqttClient) SendMessageRetained(topic string, msg []byte) error {
  273. if !c.client.IsConnected() {
  274. return fmt.Errorf("send message retained failed. (client not connected)")
  275. }
  276. token := c.client.Publish(topic, 1, true, msg)
  277. if !token.WaitTimeout(sendMsgTimeout) {
  278. return fmt.Errorf("send message retained timeout (topic:%s, msg:%s)", topic, msg)
  279. }
  280. return token.Error()
  281. }
  282. // 清除保留在Broker的消息
  283. func (c *MqttClient) CleanMessageRetained(topic string) error {
  284. if !c.client.IsConnected() {
  285. return fmt.Errorf("clean message retained failed. (client not connected)")
  286. }
  287. token := c.client.Publish(topic, 1, true, nil)
  288. if !token.WaitTimeout(sendMsgTimeout) {
  289. return fmt.Errorf("clean message retained timeout (topic:%s)", topic)
  290. }
  291. return token.Error()
  292. }
  293. // getClientStatus 通过反射强制获取 paho client 内部的连接状态(仅调试用)
  294. // 返回值: 0=disconnected, 1=disconnecting, 2=connecting, 3=reconnecting, 4=connected
  295. func (c *MqttClient) GetClientStatus() string {
  296. statusMap := map[uint32]string{
  297. 0: "disconnected",
  298. 1: "disconnecting",
  299. 2: "connecting",
  300. 3: "reconnecting",
  301. 4: "connected",
  302. }
  303. // mqtt.Client 是接口,需要先拿到底层 *client 指针
  304. rv := reflect.ValueOf(c.client).Elem() // *client -> client struct
  305. // 获取 status 字段 (类型: connectionStatus)
  306. statusField := rv.FieldByName("status")
  307. if !statusField.IsValid() {
  308. return "unknown(field not found)"
  309. }
  310. // connectionStatus 结构体中第一个字段之后是 status uint32
  311. // connectionStatus 内嵌了 sync.RWMutex,RWMutex 占据前面的空间
  312. // 实际上 status 是 connectionStatus 结构体中名为 "status" 的字段
  313. innerStatus := statusField.FieldByName("status")
  314. if !innerStatus.IsValid() {
  315. return "unknown(inner status not found)"
  316. }
  317. // 强制读取未导出字段
  318. ptr := unsafe.Pointer(innerStatus.UnsafeAddr())
  319. val := *(*uint32)(ptr)
  320. if name, ok := statusMap[val]; ok {
  321. return name
  322. }
  323. return fmt.Sprintf("unknown(%d)", val)
  324. }