fwh_mqtt_client.go 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. package fwh_mqtt
  2. import (
  3. "context"
  4. "crypto/tls"
  5. "crypto/x509"
  6. "encoding/json"
  7. "fmt"
  8. "fwh-toolbox-server/config"
  9. "fwh-toolbox-server/utils/mqttClient"
  10. "os"
  11. "strconv"
  12. "sync"
  13. "time"
  14. "go.uber.org/zap"
  15. )
  16. const (
  17. fwhClientIdPrefix = "fwh_xlog_mqtt_client_"
  18. sleepTimeout = 5 * time.Minute
  19. sendMessageTimeout = 1 * time.Minute
  20. subscriberBuffer = 64
  21. )
  22. // transport 底层 MQTT 连接(utils/mqttClient),抽象出来便于测试
  23. type transport interface {
  24. Connect() error
  25. IsConnected() bool
  26. Disconnect()
  27. SubscribeMessage(topic string, callback func(payload []byte)) error
  28. UnSubscribeMessage(topic string) error
  29. SendMessageOnce(topic string, msg []byte) error
  30. GetClientStatus() string
  31. }
  32. // Message 收到的一条消息
  33. type Message struct {
  34. Topic string
  35. Envelope FwhMqttMsg // 外层;DataArea 为通用解码结果
  36. Raw []byte // 原始字节,业务层按需重新解析(如重复键)
  37. ReceivedAt time.Time
  38. }
  39. // Handler 订阅回调;每个订阅者在独立 goroutine 中顺序调用,可以阻塞,不影响其他订阅者
  40. type Handler func(Message)
  41. type subscriber struct {
  42. id uint64
  43. handler Handler
  44. ch chan Message
  45. done chan struct{}
  46. }
  47. type topicEntry struct {
  48. subs map[uint64]*subscriber
  49. ready chan struct{} // broker 订阅完成后关闭
  50. err error
  51. }
  52. // opKind worker 串行执行的底层操作
  53. type opKind int
  54. const (
  55. opPublish opKind = iota
  56. opSubscribe
  57. opUnsubscribe
  58. )
  59. type op struct {
  60. kind opKind
  61. topic string
  62. payload []byte
  63. result chan error
  64. }
  65. // FwhMqttClient 一个环境的 FWH MQTT 客户端。
  66. // 只提供底层收发:Request(一问一答)、Publish(只请求)、Subscribe(持续监听),不包含任何业务判断。
  67. type FwhMqttClient struct {
  68. env Env
  69. tr transport
  70. ops chan op
  71. mu sync.Mutex
  72. topics map[string]*topicEntry
  73. nextID uint64
  74. }
  75. func newFwhMqttClient(env Env, tr transport) *FwhMqttClient {
  76. c := &FwhMqttClient{
  77. env: env,
  78. tr: tr,
  79. ops: make(chan op, 1000),
  80. topics: map[string]*topicEntry{},
  81. }
  82. go c.worker()
  83. return c
  84. }
  85. // Env FWH 服务器环境,每个环境使用独立的 broker、证书和客户端实例
  86. type Env string
  87. const (
  88. EnvProd Env = "prod" // 生产服
  89. EnvTest Env = "test" // 测试服
  90. )
  91. // ParseEnv 解析请求中的环境参数,空值默认为生产服
  92. func ParseEnv(s string) (Env, error) {
  93. switch Env(s) {
  94. case "", EnvProd:
  95. return EnvProd, nil
  96. case EnvTest:
  97. return EnvTest, nil
  98. default:
  99. return "", fmt.Errorf("unknown FWH MQTT env: %s", s)
  100. }
  101. }
  102. var (
  103. fwhMqttClients = map[Env]*FwhMqttClient{}
  104. fwhMqttClientsMu sync.RWMutex
  105. )
  106. // InitFwhMqttClient 初始化指定环境的 MQTT 客户端;失败时该环境不可用,不影响其他环境
  107. func InitFwhMqttClient(env Env, cfg *config.FwhMqttConfig) error {
  108. if cfg.FwhMqttBroker == "" {
  109. zap.L().Warn("FWH MQTT broker not configured, skip init.", zap.String("env", string(env)))
  110. return fmt.Errorf("FWH MQTT broker not configured (env=%s)", env)
  111. }
  112. // 证书配置
  113. caCert, err := os.ReadFile(cfg.FwhMqttCaUrl)
  114. if err != nil {
  115. zap.L().Error("Read FWH MQTT CA failed.", zap.String("env", string(env)), zap.String("err", err.Error()))
  116. return err
  117. }
  118. caCertPool := x509.NewCertPool()
  119. caCertPool.AppendCertsFromPEM(caCert)
  120. cert, err := tls.LoadX509KeyPair(cfg.FwhMqttCertUrl, cfg.FwhMqttKeyUrl)
  121. if err != nil {
  122. zap.L().Error("Load FWH MQTT cert failed.", zap.String("env", string(env)), zap.String("err", err.Error()))
  123. return err
  124. }
  125. client, err := mqttClient.CreateMqttClient(mqttClient.MqttClientOpt{
  126. Broker: cfg.FwhMqttBroker,
  127. ClientID: fwhClientIdPrefix + strconv.FormatInt(time.Now().Unix(), 10),
  128. TLS: &tls.Config{
  129. Certificates: []tls.Certificate{cert},
  130. RootCAs: caCertPool,
  131. },
  132. })
  133. if err != nil {
  134. zap.L().Error("Init FWH MQTT client failed.", zap.String("env", string(env)))
  135. return err
  136. }
  137. return registerClient(env, client)
  138. }
  139. func registerClient(env Env, tr transport) error {
  140. fwhMqttClientsMu.Lock()
  141. defer fwhMqttClientsMu.Unlock()
  142. if _, exists := fwhMqttClients[env]; exists {
  143. return fmt.Errorf("FWH MQTT client already initialized (env=%s)", env)
  144. }
  145. fwhMqttClients[env] = newFwhMqttClient(env, tr)
  146. zap.L().Info("Init FWH MQTT client success.", zap.String("env", string(env)))
  147. return nil
  148. }
  149. // GetClient 获取指定环境的 MQTT 客户端
  150. func GetClient(env Env) (*FwhMqttClient, error) {
  151. fwhMqttClientsMu.RLock()
  152. defer fwhMqttClientsMu.RUnlock()
  153. c, ok := fwhMqttClients[env]
  154. if !ok {
  155. return nil, fmt.Errorf("Fwh Mqtt Client Uninitialized (env=%s).", env)
  156. }
  157. return c, nil
  158. }
  159. /************** worker:独占底层连接,串行执行 **************/
  160. func (c *FwhMqttClient) worker() {
  161. idleTimer := time.NewTimer(sleepTimeout)
  162. defer idleTimer.Stop()
  163. for {
  164. select {
  165. case o := <-c.ops:
  166. if !idleTimer.Stop() {
  167. select {
  168. case <-idleTimer.C:
  169. default:
  170. }
  171. }
  172. idleTimer.Reset(sleepTimeout)
  173. o.result <- c.execute(o)
  174. case <-idleTimer.C:
  175. // 有活动订阅时保持连接,否则空闲断开以节省连接
  176. if c.tr.IsConnected() && !c.hasTopics() {
  177. zap.L().Info("FWH MQTT no task received within timeout, disconnect.", zap.String("env", string(c.env)))
  178. c.tr.Disconnect()
  179. }
  180. idleTimer.Reset(sleepTimeout)
  181. }
  182. }
  183. }
  184. func (c *FwhMqttClient) execute(o op) error {
  185. if err := c.tr.Connect(); err != nil {
  186. zap.L().Error("FWH MQTT connect failed.",
  187. zap.String("env", string(c.env)),
  188. zap.String("status", c.tr.GetClientStatus()),
  189. zap.String("err", err.Error()),
  190. )
  191. return fmt.Errorf("FWH MQTT connect failed: %w", err)
  192. }
  193. switch o.kind {
  194. case opPublish:
  195. return c.tr.SendMessageOnce(o.topic, o.payload)
  196. case opSubscribe:
  197. topic := o.topic
  198. return c.tr.SubscribeMessage(topic, func(payload []byte) { c.dispatch(topic, payload) })
  199. case opUnsubscribe:
  200. return c.tr.UnSubscribeMessage(o.topic)
  201. }
  202. return fmt.Errorf("unknown op %d", o.kind)
  203. }
  204. // enqueue 提交操作,返回结果 channel
  205. func (c *FwhMqttClient) enqueue(kind opKind, topic string, payload []byte) chan error {
  206. o := op{kind: kind, topic: topic, payload: payload, result: make(chan error, 1)}
  207. c.ops <- o
  208. return o.result
  209. }
  210. func waitResult(ctx context.Context, ch chan error) error {
  211. select {
  212. case err := <-ch:
  213. return err
  214. case <-ctx.Done():
  215. return ctx.Err()
  216. }
  217. }
  218. func (c *FwhMqttClient) hasTopics() bool {
  219. c.mu.Lock()
  220. defer c.mu.Unlock()
  221. return len(c.topics) > 0
  222. }
  223. /************** 对外方法 **************/
  224. // Publish 只请求:发送后立即返回
  225. func (c *FwhMqttClient) Publish(ctx context.Context, topic string, msg FwhMqttMsg) error {
  226. b, err := json.Marshal(msg)
  227. if err != nil {
  228. return err
  229. }
  230. ctx, cancel := context.WithTimeout(ctx, sendMessageTimeout)
  231. defer cancel()
  232. if err := waitResult(ctx, c.enqueue(opPublish, topic, b)); err != nil {
  233. zap.L().Error("FWH MQTT publish failed.", zap.String("env", string(c.env)), zap.String("topic", topic), zap.String("err", err.Error()))
  234. return err
  235. }
  236. return nil
  237. }
  238. // Subscribe 持续监听:同一 topic 可有多个订阅者,各自收到每条消息;返回的 cancel 只移除自己。
  239. // 首个订阅者加入时向 broker 订阅,最后一个退出时取消订阅;有订阅时连接不会空闲断开。
  240. func (c *FwhMqttClient) Subscribe(ctx context.Context, topic string, handler Handler) (cancel func(), err error) {
  241. c.mu.Lock()
  242. c.nextID++
  243. sub := &subscriber{
  244. id: c.nextID,
  245. handler: handler,
  246. ch: make(chan Message, subscriberBuffer),
  247. done: make(chan struct{}),
  248. }
  249. entry, exists := c.topics[topic]
  250. var result chan error
  251. if !exists {
  252. entry = &topicEntry{subs: map[uint64]*subscriber{}, ready: make(chan struct{})}
  253. c.topics[topic] = entry
  254. result = c.enqueue(opSubscribe, topic, nil) // 持锁入队,保证与取消订阅的先后顺序
  255. }
  256. entry.subs[sub.id] = sub
  257. c.mu.Unlock()
  258. if !exists {
  259. err := waitResult(ctx, result)
  260. c.mu.Lock()
  261. entry.err = err
  262. close(entry.ready)
  263. if err != nil && c.topics[topic] == entry {
  264. delete(c.topics, topic)
  265. // 等待超时时 broker 订阅可能稍后成功,补发取消订阅避免残留
  266. c.enqueue(opUnsubscribe, topic, nil)
  267. }
  268. c.mu.Unlock()
  269. } else {
  270. select {
  271. case <-entry.ready:
  272. case <-ctx.Done():
  273. c.removeSubscriber(topic, sub)
  274. return nil, ctx.Err()
  275. }
  276. }
  277. if entry.err != nil {
  278. zap.L().Error("FWH MQTT subscribe failed.", zap.String("env", string(c.env)), zap.String("topic", topic), zap.String("err", entry.err.Error()))
  279. return nil, fmt.Errorf("subscribe failed: %w", entry.err)
  280. }
  281. go func() {
  282. for {
  283. select {
  284. case m := <-sub.ch:
  285. sub.handler(m)
  286. case <-sub.done:
  287. return
  288. }
  289. }
  290. }()
  291. var once sync.Once
  292. return func() { once.Do(func() { c.removeSubscriber(topic, sub) }) }, nil
  293. }
  294. func (c *FwhMqttClient) removeSubscriber(topic string, sub *subscriber) {
  295. c.mu.Lock()
  296. defer c.mu.Unlock()
  297. close(sub.done)
  298. entry, ok := c.topics[topic]
  299. if !ok {
  300. return
  301. }
  302. delete(entry.subs, sub.id)
  303. if len(entry.subs) == 0 {
  304. delete(c.topics, topic)
  305. result := c.enqueue(opUnsubscribe, topic, nil)
  306. go func() {
  307. if err := <-result; err != nil {
  308. zap.L().Warn("FWH MQTT unsubscribe failed.", zap.String("env", string(c.env)), zap.String("topic", topic), zap.String("err", err.Error()))
  309. }
  310. }()
  311. }
  312. }
  313. // dispatch 在 paho 回调线程中执行:解析一次外层,非阻塞分发给各订阅者
  314. func (c *FwhMqttClient) dispatch(topic string, payload []byte) {
  315. m := Message{Topic: topic, Raw: payload, ReceivedAt: time.Now()}
  316. if err := json.Unmarshal(payload, &m.Envelope); err != nil {
  317. zap.L().Warn("FWH MQTT message unmarshal failed.", zap.String("topic", topic), zap.String("payload", string(payload)), zap.String("err", err.Error()))
  318. return
  319. }
  320. c.mu.Lock()
  321. entry := c.topics[topic]
  322. var subs []*subscriber
  323. if entry != nil {
  324. for _, s := range entry.subs {
  325. subs = append(subs, s)
  326. }
  327. }
  328. c.mu.Unlock()
  329. for _, s := range subs {
  330. select {
  331. case <-s.done:
  332. case s.ch <- m:
  333. default:
  334. zap.L().Warn("FWH MQTT subscriber buffer full, dropping message.", zap.String("topic", topic), zap.Uint("cmdType", m.Envelope.CmdType))
  335. }
  336. }
  337. }
  338. // RequestOpt 一问一答参数
  339. type RequestOpt struct {
  340. Topic string // 发送 topic
  341. Msg FwhMqttMsg // 发送内容
  342. ResTopic string // 应答 topic
  343. Match func(Message) bool // 应答匹配条件,由调用方定义;为 nil 时取第一条消息
  344. Timeout time.Duration // 等待应答超时,默认 sendMessageTimeout
  345. }
  346. // Request 一问一答:先订阅应答 topic,再发送,返回第一条满足 Match 的消息
  347. func (c *FwhMqttClient) Request(ctx context.Context, opt RequestOpt) (*Message, error) {
  348. timeout := opt.Timeout
  349. if timeout <= 0 {
  350. timeout = sendMessageTimeout
  351. }
  352. ctx, cancelCtx := context.WithTimeout(ctx, timeout)
  353. defer cancelCtx()
  354. resCh := make(chan Message, 1)
  355. cancelSub, err := c.Subscribe(ctx, opt.ResTopic, func(m Message) {
  356. if opt.Match != nil && !opt.Match(m) {
  357. return
  358. }
  359. select {
  360. case resCh <- m:
  361. default:
  362. }
  363. })
  364. if err != nil {
  365. return nil, err
  366. }
  367. defer cancelSub()
  368. if err := c.Publish(ctx, opt.Topic, opt.Msg); err != nil {
  369. return nil, err
  370. }
  371. select {
  372. case m := <-resCh:
  373. return &m, nil
  374. case <-ctx.Done():
  375. return nil, fmt.Errorf("FWH MQTT received message timeout")
  376. }
  377. }