| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375 |
- package mqttClient
- import (
- "crypto/tls"
- "errors"
- "fmt"
- "log"
- "net/url"
- "os"
- "reflect"
- "time"
- "unsafe"
- mqtt "github.com/eclipse/paho.mqtt.golang"
- "go.uber.org/zap"
- )
- type MqttClientOpt struct {
- Broker string // 服务端地址
- ClientID string // (可选) 客户端名称
- TLS *tls.Config
- }
- type MqttClient struct {
- client mqtt.Client
- clientOptions *mqtt.ClientOptions
- subscribeMap map[string]func(payload []byte)
- clientID string // 客户端名称
- }
- const (
- connectTimeout = 30 * time.Second // 30s
- reconnectTimeout = 10 * time.Second // 10s
- disconnectTimeout = 10 * 1000 // 10s
- subscribeMsgTimeout = 30 * time.Second // 30s
- sendMsgTimeout = 30 * time.Second // 30s
- )
- // 创建客户端
- func CreateMqttClient(opt MqttClientOpt) (*MqttClient, error) {
- // 初始化前设置(放在 InitFwhMqttClient 开头或 init() 中)
- mqtt.DEBUG = log.New(os.Stdout, "[paho-DEBUG] ", log.Ltime|log.Lmicroseconds)
- mqtt.WARN = log.New(os.Stdout, "[paho-WARN] ", log.Ltime|log.Lmicroseconds)
- mqtt.ERROR = log.New(os.Stderr, "[paho-ERROR] ", log.Ltime|log.Lmicroseconds)
- // 创建客户端选项
- opts := mqtt.NewClientOptions()
- opts.AddBroker(opt.Broker)
- clientID := "go_mqtt_client"
- if len(opt.ClientID) > 0 {
- clientID = opt.ClientID
- }
- opts.SetClientID(clientID)
- opts.SetAutoReconnect(true) // 自动重连
- opts.SetCleanSession(true) // 不保留会话
- opts.SetTLSConfig(opt.TLS)
- mqttClient := MqttClient{
- subscribeMap: map[string]func(payload []byte){},
- clientID: clientID,
- }
- // 连接成功回调
- opts.OnConnect = func(c mqtt.Client) {
- zap.L().Info("Connected to MQTT broker", zap.String("clientID", clientID))
- // 重新订阅丢失的 Subscribe
- mqttClient.ResumeSubscribe()
- }
- // 连接丢失回调
- opts.OnConnectionLost = func(c mqtt.Client, err error) {
- fmt.Printf("%s Connection lost: %s\n", clientID, err.Error())
- zap.L().Info("MQTT Connection lost", zap.String("clientID", clientID), zap.String("err", err.Error()))
- }
- // 重连回调
- opts.OnReconnecting = func(c mqtt.Client, opts *mqtt.ClientOptions) {
- zap.L().Info("MQTT Reconnecting", zap.String("clientID", clientID))
- }
- // 尝试连接回调
- opts.OnConnectAttempt = func(broker *url.URL, tlsCfg *tls.Config) *tls.Config {
- zap.L().Info("MQTT Attempt Connect", zap.String("clientID", clientID), zap.String("Broker", broker.String()))
- return tlsCfg
- }
- // 连接事件回调
- opts.OnConnectionNotification = func(c mqtt.Client, cn mqtt.ConnectionNotification) {
- zap.L().Info("MQTT Connecting Notification", zap.String("clientID", clientID), zap.Int64("Notification", int64(cn.Type())))
- }
- // 创建客户端
- mqttClient.client = mqtt.NewClient(opts)
- mqttClient.clientOptions = opts
- return &mqttClient, nil
- }
- func (c *MqttClient) RecreateClient() error {
- if c.clientOptions == nil {
- return errors.New("RecreateClient failed, ClientOptions is nil.")
- }
- // 尝试断开旧连接(非阻塞)
- oldClient := c.client
- go func() {
- if oldClient != nil {
- done := make(chan struct{})
- go func() {
- oldClient.Disconnect(disconnectTimeout)
- close(done)
- }()
- select {
- case <-done:
- case <-time.After(15 * time.Second):
- zap.L().Warn("[MQTT] old client disconnect timeout, abandoning")
- }
- }
- }()
- // 重新创建客户端
- c.client = mqtt.NewClient(c.clientOptions)
- zap.L().Info("[MQTT] create new client")
- return nil
- }
- func (c *MqttClient) Connect() error {
- isConnected := c.client.IsConnected()
- isOpen := c.client.IsConnectionOpen()
- zap.L().Debug("[MQTT] Connect() status check",
- zap.Bool("IsConnected", isConnected),
- zap.Bool("IsConnectionOpen", isOpen),
- )
- // 底层连接已活跃,无需再连
- if isOpen {
- return nil
- }
- // IsConnected=true 但连接不活跃 → 正在重连中,等待恢复
- if isConnected && !isOpen {
- zap.L().Info("[MQTT] client is reconnecting, waiting for connection to restore")
- deadline := time.Now().Add(reconnectTimeout)
- for time.Now().Before(deadline) {
- if c.client.IsConnectionOpen() {
- return nil
- }
- // 如果 IsConnected 也变成 false 了,说明重连失败/放弃了
- if !c.client.IsConnected() {
- zap.L().Warn("[MQTT] client lost connection during reconnect wait, will re-connect")
- break
- }
- time.Sleep(500 * time.Millisecond)
- }
- // 等待超时或重连失败,检查最终状态
- if c.client.IsConnectionOpen() {
- return nil
- }
- // 强制断开后重连(清除 disconnecting 卡死状态)
- // zap.L().Warn("[MQTT] reconnect failed, forcing disconnect and re-connect")
- // c.client.Disconnect(0)
- // time.Sleep(100 * time.Millisecond)
- }
- // 真正断开状态,执行连接
- token := c.client.Connect()
- if !token.WaitTimeout(connectTimeout) {
- if err := c.RecreateClient(); err != nil {
- return fmt.Errorf("recreate client failed (%s)", c.clientID)
- }
- token = c.client.Connect()
- if !token.WaitTimeout(connectTimeout) {
- return fmt.Errorf("connect timeout (%s)", c.clientID)
- }
- }
- return token.Error()
- }
- // 断开连接
- func (c *MqttClient) Disconnect() {
- // 清空订阅
- c.CleanSubscribe()
- // 安全断开,等待最多 10 秒让队列消息发送完
- c.client.Disconnect(disconnectTimeout)
- }
- // 检查是否连接
- func (c *MqttClient) IsConnected() bool {
- return c.client.IsConnected()
- }
- // 恢复订阅消息
- func (c *MqttClient) ResumeSubscribe() {
- for k, v := range c.subscribeMap {
- c.subscribeMessage(k, v)
- }
- }
- // 清空所有订阅
- func (c *MqttClient) CleanSubscribe() {
- for k := range c.subscribeMap {
- delete(c.subscribeMap, k)
- }
- }
- // 订阅消息
- func (c *MqttClient) subscribeMessage(topic string, callback func(payload []byte)) error {
- if !c.client.IsConnected() {
- return fmt.Errorf("subscribe failed. (client not connected)")
- }
- token := c.client.Subscribe(topic, 1, func(c mqtt.Client, m mqtt.Message) {
- callback(m.Payload())
- })
- if !token.WaitTimeout(subscribeMsgTimeout) {
- return fmt.Errorf("subscribe timeout (topic:%s)", topic)
- }
- err := token.Error()
- if err != nil {
- return err
- }
- return nil
- }
- // 订阅消息
- func (c *MqttClient) SubscribeMessage(topic string, callback func(payload []byte)) error {
- err := c.subscribeMessage(topic, callback)
- if err != nil {
- return err
- }
- // 备份subscribe
- c.subscribeMap[topic] = callback
- return nil
- }
- // 取消订阅
- func (c *MqttClient) UnSubscribeMessage(topic string) error {
- if !c.client.IsConnected() {
- return fmt.Errorf("unsubscribe failed. (client not connected)")
- }
- token := c.client.Unsubscribe(topic)
- if !token.WaitTimeout(subscribeMsgTimeout) {
- return fmt.Errorf("unsubscribe timeout (topic:%s)", topic)
- }
- err := token.Error()
- if err != nil {
- return err
- }
- delete(c.subscribeMap, topic)
- return nil
- }
- // 发送消息,只尝试一次,不管是否送达
- func (c *MqttClient) SendMessageOnce(topic string, msg []byte) error {
- if !c.client.IsConnected() {
- return fmt.Errorf("send message once failed. (client not connected)")
- }
- token := c.client.Publish(topic, 0, false, msg)
- if !token.WaitTimeout(sendMsgTimeout) {
- return fmt.Errorf("send message once timeout (topic:%s, msg:%s)", topic, msg)
- }
- return token.Error()
- }
- // 发送消息,没收到 ACK 就重发,至少送到一次,可能收到多次
- func (c *MqttClient) SendMessage(topic string, msg []byte, retained bool) error {
- if !c.client.IsConnected() {
- return fmt.Errorf("send message failed. (client not connected)")
- }
- token := c.client.Publish(topic, 1, retained, msg)
- if !token.WaitTimeout(sendMsgTimeout) {
- return fmt.Errorf("send message timeout (topic:%s, msg:%s)", topic, msg)
- }
- return token.Error()
- }
- // 发送消息,严格一次,不重复,不丢失
- func (c *MqttClient) SendMessageMustOnce(topic string, msg []byte, retained bool) error {
- if !c.client.IsConnected() {
- return fmt.Errorf("send message must once failed. (client not connected)")
- }
- token := c.client.Publish(topic, 2, retained, msg)
- if !token.WaitTimeout(sendMsgTimeout) {
- return fmt.Errorf("send message must once timeout (topic:%s, msg:%s)", topic, msg)
- }
- return token.Error()
- }
- // 发送消息,没收到 ACK 就重发,至少送到一次,可能收到多次,并保留消息在Broker,新定义的设备会收到消息
- func (c *MqttClient) SendMessageRetained(topic string, msg []byte) error {
- if !c.client.IsConnected() {
- return fmt.Errorf("send message retained failed. (client not connected)")
- }
- token := c.client.Publish(topic, 1, true, msg)
- if !token.WaitTimeout(sendMsgTimeout) {
- return fmt.Errorf("send message retained timeout (topic:%s, msg:%s)", topic, msg)
- }
- return token.Error()
- }
- // 清除保留在Broker的消息
- func (c *MqttClient) CleanMessageRetained(topic string) error {
- if !c.client.IsConnected() {
- return fmt.Errorf("clean message retained failed. (client not connected)")
- }
- token := c.client.Publish(topic, 1, true, nil)
- if !token.WaitTimeout(sendMsgTimeout) {
- return fmt.Errorf("clean message retained timeout (topic:%s)", topic)
- }
- return token.Error()
- }
- // getClientStatus 通过反射强制获取 paho client 内部的连接状态(仅调试用)
- // 返回值: 0=disconnected, 1=disconnecting, 2=connecting, 3=reconnecting, 4=connected
- func (c *MqttClient) GetClientStatus() string {
- statusMap := map[uint32]string{
- 0: "disconnected",
- 1: "disconnecting",
- 2: "connecting",
- 3: "reconnecting",
- 4: "connected",
- }
- // mqtt.Client 是接口,需要先拿到底层 *client 指针
- rv := reflect.ValueOf(c.client).Elem() // *client -> client struct
- // 获取 status 字段 (类型: connectionStatus)
- statusField := rv.FieldByName("status")
- if !statusField.IsValid() {
- return "unknown(field not found)"
- }
- // connectionStatus 结构体中第一个字段之后是 status uint32
- // connectionStatus 内嵌了 sync.RWMutex,RWMutex 占据前面的空间
- // 实际上 status 是 connectionStatus 结构体中名为 "status" 的字段
- innerStatus := statusField.FieldByName("status")
- if !innerStatus.IsValid() {
- return "unknown(inner status not found)"
- }
- // 强制读取未导出字段
- ptr := unsafe.Pointer(innerStatus.UnsafeAddr())
- val := *(*uint32)(ptr)
- if name, ok := statusMap[val]; ok {
- return name
- }
- return fmt.Sprintf("unknown(%d)", val)
- }
|