這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。
rabbitmq訊息模式
rabbitmq中進行訊息控制的組建可以分為以下幾部分:
- exchange:rabbitmq中的路由組件,控制訊息的轉寄路徑;
- queue:rabbitmq的訊息佇列,可以有多個消費者從隊列中讀取訊息;
- consumer:訊息的消費者;
rabbitmq在使用過程中可以單獨使用queue進行訊息傳遞(例如celery就可以使用單個queue進行多對多的訊息傳遞),也利用exchange與queue構建多種訊息模式,主要包括fanout、direct和topic方式,模式的使用方式在此放一張圖,不再此做詳細解釋。
我在使用的rabbitmq的過程中,主要是進行訊息的廣播及主題訂閱:
[producer] -> [exchange] ->fanout-> [queue of consumer] -> [consumer] | /|\ ------->[exchange] ->topic------
不同的裝置串連到rabbitmq中建立自己的queue,將queue綁定的兩個不同的exchange,分別接收廣播訊息及主題訊息。通過配置queue的持久化及訊息到期時間,則可以在裝置短暫下線的情況下,將訊息緩衝在queue中,之後上線後再從queue中讀取訊息。
rabbitmq用戶端
rabbitmq用戶端本質上是實現amqp協議的通訊過程,golang的基礎package使用的是github.com/streadway/amqp
。
在此主要對用戶端構建中的一些問題進行陳述,詳細的用戶端構建代碼請參見:rabbitmq_client.go
建立queue
exchange和queue實際上都是通過amqp協議進行建立的,如果在建立過程時,rabbitmq中已經有相同名稱的exchange或queue但屬性不則會建立失敗。通常情況下exchange的屬性不會變化,但是queue可能會修改到期時間、訊息TTL等屬性,因此實現過程中,若queue建立不成功則進行刪除後再建立(在我的應用程式情境中queue與消費者綁定,因此不存在誤刪在使用中的queue的問題):
func (clt *Client) queInit(server *broker, ifFresh bool) (err error) {var num intch := clt.chif ifFresh {num, err = ch.QueueDelete(server.quePrefix+"."+clt.device,false,false,false,)if err != nil {return}log.Println("[RABBITMQ_CLIENT]", clt.device, "queue deleted with", num, "message purged")}args := make(amqp.Table)args["x-message-ttl"] = messageTTLargs["x-expires"] = queueExpireq, err := ch.QueueDeclare(server.quePrefix+"."+clt.device, // nametrue, // durablefalse, // delete when ususedfalse, // exclusivefalse, // no-waitargs, // arguments) // 注意在此配置的兩個參數,詳細用意請參見 http://next.rabbitmq.com/ttl.htmlif err != nil {return}for _, topic := range server.topics {err = ch.QueueBind(q.Name,topic.keyPrefix+"."+clt.device,topic.chanName,false,nil,)if err != nil {return}}clt.que = qreturn}
訊息接收
對於消費者訊息的接收過程如下所示:
msgs, err := clt.ch.Consume(clt.que.Name, // queueclt.device, // consumerfalse, // auto ackfalse, // exclusivefalse, // no localfalse, // no waitnil, // args)if err != nil {clt.Close()log.Println("[RABBITMQ_CLIENT]", "Start consume ERROR:", err)return nil}clt.msgs = msgsclt.pubChan = make(chan *publishMsg, 4)go func() {cc := make(chan *amqp.Error)e := <-clt.ch.NotifyClose(cc)log.Println("[RABBITMQ_CLIENT]", "channel close error:", e.Error())clt.cancel()}()go func() {for d := range msgs {msg := d.BodymsgProcess(d.Exchange, msg)d.Ack(false)}}()
通過ch.Consume
調用可以得到一個接收訊息的msgs channel,在此沒有配置auto ack,而是在訊息處理結束之後,通過調用d.Ack(false)
反饋ACK,這樣可以保證訊息在被處理之後,再進行確認。消費過程中,還調用ch.NotifyClose(cc)
對amqp.Channel
的關閉進行偵聽。
注意:在一個gorontinue中同時對msgs和notifyClose兩個channel進行讀取可能會導致死結。因為msgs被關閉就會結束相應的gorontinue,此時notifyClose因為沒有接收者,而在amqp.channel
關閉的過程中出現死結。
訊息發送
在amqp的訊息發送過程中,其對於訊息的確認機制略有些蛋疼。因為在發送的時候不可配置發送的訊息id,但在接收確認時,訊息id是按照自然數遞增的,也就是說寄件者需要按照自然數遞增的順序自己維護髮送的訊息id。相關代碼如下所示:
func (clt *Client) publishProc() {ticker := time.NewTicker(tickTime)deliveryMap := make(map[uint64]*publishMsg)defer func() {atomic.AddInt32(&clt.onPublish, -1)ticker.Stop()for _, msg := range deliveryMap {msg.ackErr = errCancelmsg.cancel()}}()var deliveryTag uint64 = 1var ackTag uint64 = 1var pMsg *publishMsgfor {select {case <-clt.ctx.Done():returncase pMsg = <-clt.pubChan:pMsg.startTime = time.Now()err := clt.sendPublish(pMsg.topicId, pMsg.keySuffix, pMsg.msg, pMsg.expire)if err != nil {pMsg.ackErr = errpMsg.cancel()}deliveryMap[deliveryTag] = pMsgdeliveryTag++case c, ok := <-clt.confirm:if !ok {log.Println("[RABBITMQ_CLIENT]", "client Publish notify channel error")return}pMsg = deliveryMap[c.DeliveryTag]// fmt.Println("DeliveryTag:", c.DeliveryTag)delete(deliveryMap, c.DeliveryTag)if c.Ack {pMsg.ackErr = nilpMsg.cancel()} else {pMsg.ackErr = errNackpMsg.cancel()}case <-ticker.C:now := time.Now()for {if len(deliveryMap) == 0 {break}pMsg = deliveryMap[ackTag]if pMsg != nil {if now.Sub(pMsg.startTime.Add(pubTime)) > 0 {pMsg.ackErr = errTimeoutpMsg.cancel()delete(deliveryMap, ackTag)} else {break}}ackTag++}}}}
發送過程的構造要點:
- 使用一個
map[uint64]*publishMsg
儲存已經發送的訊息,map的鍵為訊息的id;
- 接收到確認訊息後,通過訊息的反饋機制反饋確認資訊,並從map中刪除訊息;
- 在每一個tick,按照遞增的id檢查map中是否有逾時訊息,通過訊息的反饋機制反饋逾時資訊;
- 在協程退出時向每個訊息發送反饋資訊,並刪除訊息。
需要注意的是,訊息反饋並沒有使用channel,因為訊息的接收者可能因為逾時不再偵聽channel,從而導致發送過程出現阻塞。可以用長度不為0的反饋channel使得發送過程不阻塞,但是著需要等待gc後才能釋放反饋channel的記憶體。因此在此並沒有使用channel接收反饋,而是通過context
的事件來告知發送方訊息發送過程結束,反饋資訊則提前寫在publishMsg
的ackErr
中。
總結
作為golang的入門級選手,在實現rabbitmq用戶端過程中還是踩了一些坑,最後的實現還是可以算是高效可靠。rabbitmq的庫本身有心跳機制來維持與伺服器之間的串連,但依據實現mqtt用戶端的經驗,還是自己實現了心跳來保障用戶端上層串連的可靠性。因此在接收和發送兩方面,該用戶端實現還是經受住了考驗,歡迎大家參考。