C++消息框架-基於sigslot


一、簡介

上一篇文章Qt信號槽-原理分析主要講述了Qt的信號槽實現原理,當然除了Qt的信號槽以外,還有boost的signals,sigslot和sigc++等等,都是非常不錯的信號槽學習資料

  • boost的信號槽機制很強大,但是依賴了其他模塊,而且對於大多數人來說,標准C++已經夠用
  • sigc++功能也不錯,但是文件數量比較多
  • sigslot只有一個頭文件,非常輕量,而且現有功能夠我們使用

了解sigslot用法可以參考sigslots的簡單例子這篇文章,使用起來還是相對簡單

本篇文章我們主要是使用sigslots來做一個簡單的消息框架,主要是進行多個模塊之間消息通信,當然也可以是插件之間通信。

我們的框架總的來說是一個簡化版的消息通信機制,學習起來也比較輕松,如果用於實際的工程項目的話,還需要進一步的優化。

如下圖所示,是我自己畫的類圖,我們通過signal1來發送消息,並傳遞給所有的Receiver,這里的接收者簡單來說可以是一個類,如果是想復雜一些,也可以是一個插件,后邊我會單獨講述怎么加載插件dll

二、消息

通常不同模塊之間傳遞消息時我們需要定義一個消息結構,他可以作為函數回調時的參數,然后我們會根據參數中的唯一標識,來區分不同的消息,或者判斷是不是我們想要處理的消息。

/** 消息結構*/
struct Message
{
    std::string m_strMessage; ///消息類型  唯一ID
    void *      m_pUserData;  ///發送的數據
};

如Message結構中的m_strMessage變量,他唯一標識了消息的類型,我們只需要判斷是我們想處理的消息類型時,執行處理代碼即可。

sigslot庫中的信號最多支持8個參數,可是在我門日常的開發工作中,可能會存在一些特殊的場景,超過8個參數;除此之外,根據參數類型的不同,往復雜里寫我們可能需要寫大量的適配工作。在這里我們使用一個簡單的小技巧,通過void *來轉發我們的數據,也就是m_pUserData,這樣不管多少數據,我們都可以封裝到一個變量中。

m_pUserData里邊我們可以存儲任意類型的數據,只要我們在處理事件的時候知道怎么取出數據即可。

三、發送者

知道觀察者模式的同學應該都知道,被觀察的對象(Subject)維護了一個觀察者(Observer)列表,當我們的被觀察者發生變化的時候,被觀察者可以遍歷自己維護的觀察者列表,然后將變化通知給觀察者。同樣的我們這個框架也類似於這樣的設計,只不過我們的發送者沒有維護接收者列表,而是通過信號槽的綁定機制,把發送者的發送函數綁定到了接收者的接收函數,而且是一對多綁定,也就是說我們的信號可以對多個槽。

這樣的設計下,發送者和接收者還是有一定的耦合,后邊有時間優化的話,我會引入一個第三方的管理者,幫助我們讓發送者和接收者進行關聯,這樣也能提供最大的靈活性。

如下是發送者的代碼

class Sender
{
public:
    void sendMessage(const std::string & = "", void * = 0);
    virtual void addReceiver(Receiver *);
    virtual void removeReceiver(Receiver *);

private:
    sigslot::signal1<Message *> m_pSender;
};

發送者包含3個接口,發送消息、添加接收者和移除接收者。而最重要的地方當屬我們的m_pSender變量,他是sigslot庫封裝的信號,這個庫總共提供了8種信號,但是我們只使用參數為1個的信號,因為我們把參數封裝成了一個結構,也就是說我們的參數被包裝成了一個對象。

下面我們來分析下這三個函數

1、發送消息函數

發送消息時,我們需要指定消息的id和消息的內容,並構造為一個Message對象,作為信號參數發送出去,這樣槽函數就可以收到我們發送的內容。

特別注意,Message對象的銷毀是在所有槽函數執行完畢以后

void Sender::sendMessage(const std::string & msgID, void * data)
{
    Message msg;
    msg.m_strMessage = msgID;
    msg.m_pUserData = data;

    m_pSender(&msg);//消息的接收者執行完后  msg被銷毀
}

2、新增一個接收者函數

新增接收者時,我們只需要使用connect把接收者的函數綁定到我們的信號上即可。是不是特別簡單呢!

void Sender::addReceiver(Receiver * receiver)
{
    m_pSender.connect(receiver, &Receiver::onMessage);
}

3、移除一個接收者函數

移除接受者時,我們只需要使用disconnect把接收者從綁定的接收者列表中移除即可。

void Sender::removeReceiver(Receiver * receiver)
{
    m_pSender.disconnect(receiver);
}

四、接收者

sigslot庫要求我們,如果想要被signals信號連接,則我們的類必須從sigslot::has_slots<>繼承,這里我們封裝了一個Receiver類,方便后續我們寫更多的功能類。這個類里我添加了一個onMessage函數,這個函數就是我們處理信號的回調函數,當signals發送信號時,onMessage函數就會被調用,我們在這里處理自己關注的事件即可。

class Receiver : public sigslot::has_slots<>
{
public:
    virtual void onMessage(Message *) = 0;
};

我們在寫新功能時,只需要繼承Receiver類,並實現onMessage函數即可。

Message就是我們發送信號時構造的對象,里邊包含了消息的類型ID和用戶數據,我們只需要根據消息ID就可以知道,這個消失是否是我們需要處理的,如果需要處理,那我們將需要小心翼翼的從void *中取出相關用戶數據,進行處理。

例如下面代碼,是一個簡單的消息頁面,當我們收到消息回調時,我們通過判斷消息ID,他就是我們需要處理的消息NEW_ITEM_REPORT,然后我們打印了一句話,

這里只是簡單舉了一個例子,實際開發中,代碼復雜度往往都比較高

class newsPage : public Receiver{
public:
    newsPage(Sender * sender) {
        sender->addReceiver(this);//把自己加入到消息接收者隊列中
    }
    virtual void onMessage(Message * msg)    {
        if (msg->m_strMessage == "NEW_ITEM_REPORT")        {
            std::cout << "收到一條新消息:";
        }
    }
};

五、功能測試

下面我們寫兩個實際的消息接收類,來測試下消息框架

1、消息接收類

a、測試類1

消息接收類我們必須從Receiver來繼承,並且需要把自己添加到信號對象的消息接收列表中。

處理消息時,當我們發現消息ID是字符串“1”時,是我們要處理的消息,則打印消息內容

class testReceiver1 : public Receiver{
public:
    testReceiver1(Sender * sender) {
        sender->addReceiver(this);//把自己加入到消息接收者隊列中
    }
    virtual void onMessage(Message * msg)    {
        if (msg->m_strMessage == "1")        {
            std::cout << "testReceiver1:" << (char *)msg->m_pUserData << "\n";
        }
    }
};

b、測試類2

消息接收類2同類1一樣,只是處理消息時,判斷的消息ID不一樣,這里不做解釋,

class testReceiver2 : public Receiver{
public:
    testReceiver2(Sender * sender) {
        sender->addReceiver(this);//把自己加入到消息接收者隊列中
    }
    virtual void onMessage(Message * msg)    {
        if (msg->m_strMessage == "2")        {
            std::cout << "testReceiver2:" << (char *)msg->m_pUserData << "\n";
        }
    }
};

2、測試代碼

測試代碼如下,我們構造了一個Sender發送者,並聲明了兩個消息接收對象,然后直接使用send對象開始發送消息

實際使用過程中,Sender可能不會這樣直接暴露出來,通常是通過一個單例來進行管理

int main()
{
    Sender send;
    testReceiver1 rece1(&send);
    testReceiver2 rece2(&send);

    send.sendMessage("1", "Receiver1 deal");
    send.sendMessage("2", "Receiver2 deal");

    getchar();

    return 0;
}

3、測試結果

最終測試結果如下

  • 接收者1處理了消息類型為“1”的事件,並打印了testReceiver1:send2Receiver1
  • 接收者2處理了消息類型為“2”的事件,並打印了testReceiver2:send2Receiver2

六、源碼

需要源碼的留郵箱,現在的csdn簡直太坑爹了。。。


如果您覺得文章不錯,不妨給個 打賞,寫作不易,感謝各位的支持。您的支持是我最大的動力,謝謝!!!




很重要--轉載聲明

  1. 本站文章無特別說明,皆為原創,版權所有,轉載時請用鏈接的方式,給出原文出處。同時寫上原作者:朝十晚八 or Twowords

  2. 如要轉載,請原文轉載,如在轉載時修改本文,請事先告知,謝絕在轉載時通過修改本文達到有利於轉載者的目的。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM