C++訊息框架-基於sigslot

朝十晚八發表於2019-05-07

一、簡介

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

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

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

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

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

如下圖所示,是我自己畫的類圖,我們通過signal1來傳送訊息,並傳遞給所有的Receiver,這裡的接收者簡單來說可以是一個類,如果是想複雜一些,也可以是一個外掛,後邊我會單獨講述怎麼載入外掛dll



C++訊息框架-基於sigslot


二、訊息

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

/** 訊息結構*/
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
C++訊息框架-基於sigslot

六、原始碼

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




轉載宣告:本站文章無特別說明,皆為原創,版權所有,轉載請註明:朝十晚八 or Twowords


相關文章