mqttClient.go 10 KB

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