rabbitmq 用戶端golang實戰

來源:互聯網
上載者:User
這是一個建立於 的文章,其中的資訊可能已經有所發展或是發生改變。

rabbitmq訊息模式

rabbitmq中進行訊息控制的組建可以分為以下幾部分:

  1. exchange:rabbitmq中的路由組件,控制訊息的轉寄路徑;
  2. queue:rabbitmq的訊息佇列,可以有多個消費者從隊列中讀取訊息;
  3. 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++}}}}

發送過程的構造要點:

  1. 使用一個map[uint64]*publishMsg儲存已經發送的訊息,map的鍵為訊息的id;
  2. 接收到確認訊息後,通過訊息的反饋機制反饋確認資訊,並從map中刪除訊息;
  3. 在每一個tick,按照遞增的id檢查map中是否有逾時訊息,通過訊息的反饋機制反饋逾時資訊;
  4. 在協程退出時向每個訊息發送反饋資訊,並刪除訊息。

需要注意的是,訊息反饋並沒有使用channel,因為訊息的接收者可能因為逾時不再偵聽channel,從而導致發送過程出現阻塞。可以用長度不為0的反饋channel使得發送過程不阻塞,但是著需要等待gc後才能釋放反饋channel的記憶體。因此在此並沒有使用channel接收反饋,而是通過context的事件來告知發送方訊息發送過程結束,反饋資訊則提前寫在publishMsgackErr中。

總結

作為golang的入門級選手,在實現rabbitmq用戶端過程中還是踩了一些坑,最後的實現還是可以算是高效可靠。rabbitmq的庫本身有心跳機制來維持與伺服器之間的串連,但依據實現mqtt用戶端的經驗,還是自己實現了心跳來保障用戶端上層串連的可靠性。因此在接收和發送兩方面,該用戶端實現還是經受住了考驗,歡迎大家參考。

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.