linux下使用hiredis異步API實現sub/pub消息訂閱和發佈的功能

本文轉載自鏈接:
http://blog.csdn.net/chenzba/article/details/51224715

最近使用redis的c接口——hiredis,使客戶端與redis服務器通信,實現消息訂閱和發佈(PUB/SUB)的功能,我把遇到的一些問題和解決方法列出來供大家學習。
廢話不多說,先貼代碼。
redis_publisher.h

/*************************************************************************
    > File Name: redis_publisher.h
    > Author: chenzengba
    > Mail: [email protected] 
    > Created Time: Sat 23 Apr 2016 10:15:09 PM CST
    > Description: 封裝hiredis,實現消息發佈給redis功能
 ***********************************************************************/

#ifndef REDIS_PUBLISHER_H
#define REDIS_PUBLISHER_H

#include <stdlib.h>
#include <hiredis/async.h>
#include <hiredis/adapters/libevent.h>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <boost/tr1/functional.hpp>

class CRedisPublisher
{
public:    
    CRedisPublisher();
    ~CRedisPublisher();

    bool init();
    bool uninit();
    bool connect();
    bool disconnect();

    bool publish(const std::string channel_name, 
        const std::string message);

private:
     // 下面三個回調函數供redis服務調用
    // 連接回調
    static void connect_callback(const redisAsyncContext *redis_context,
        int status);

    // 斷開連接的回調
    static void disconnect_callback(const redisAsyncContext *redis_context,
        int status);

    // 執行命令回調
    static void command_callback(redisAsyncContext *redis_context,
        void *reply, void *privdata);

    // 事件分發線程函數
    static void *event_thread(void *data);
    void *event_proc();

private:
     // libevent事件對象
    event_base *_event_base;
    // 事件線程ID
    pthread_t _event_thread;
    // 事件線程的信號量
    sem_t _event_sem;
    // hiredis異步對象
    redisAsyncContext *_redis_context;
};

#endif

redis_publisher.cpp

/*************************************************************************
    > File Name: redis_publisher.cpp
    > Author: chenzengba
    > Mail: [email protected] 
    > Created Time: Sat 23 Apr 2016 10:15:09 PM CST
    > Description: 
 ***********************************************************************/

#include <stddef.h>
#include <assert.h>
#include <string.h>
#include "redis_publisher.h"

CRedisPublisher::CRedisPublisher():_event_base(0), _event_thread(0),
_redis_context(0)
{
}

CRedisPublisher::~CRedisPublisher()
{
}

bool CRedisPublisher::init()
{
    // initialize the event
    _event_base = event_base_new();    // 創建libevent對象
    if (NULL == _event_base)
    {
        printf(": Create redis event failed.\n");
        return false;
    }

    memset(_event_sem, 0, sizeof(_event_sem));
    int ret = sem_init(_event_sem, 0, 0);
    if (ret != 0)
    {
        printf(": Init sem failed.\n");
        return false;
    }

    return true;
}

bool CRedisPublisher::uninit()
{
    _event_base = NULL;

    sem_destroy(_event_sem);   
    return true;
}

bool CRedisPublisher::connect()
{
    // connect redis
    _redis_context = redisAsyncConnect("127.0.0.1", 6379);    // 異步連接到redis服務器上,使用默認端口
    if (NULL == _redis_context)
    {
        printf(": Connect redis failed.\n");
        return false;
    }

    if (_redis_context->err)
    {
        printf(": Connect redis error: %d, %s\n", 
            _redis_context->err, _redis_context->errstr);    // 輸出錯誤信息
        return false;
    }

    // attach the event
    redisLibeventAttach(_redis_context, _event_base);    // 將事件綁定到redis context上,使設置給redis的回調跟事件關聯

    // 創建事件處理線程
    int ret = pthread_create(_event_thread, 0, CRedisPublisher::event_thread, this);
    if (ret != 0)
    {
        printf(": create event thread failed.\n");
        disconnect();
        return false;
    }

    // 設置連接回調,當異步調用連接後,服務器處理連接請求結束後調用,通知調用者連接的狀態
    redisAsyncSetConnectCallback(_redis_context, 
        CRedisPublisher::connect_callback);

    // 設置斷開連接回調,當服務器斷開連接後,通知調用者連接斷開,調用者可以利用這個函數實現重連
    redisAsyncSetDisconnectCallback(_redis_context,
        CRedisPublisher::disconnect_callback);

    // 啓動事件線程
    sem_post(_event_sem);
    return true;
}

bool CRedisPublisher::disconnect()
{
    if (_redis_context)
    {
        redisAsyncDisconnect(_redis_context);
        redisAsyncFree(_redis_context);
        _redis_context = NULL;
    }

    return true;
}

bool CRedisPublisher::publish(const std::string channel_name,
    const std::string message)
{
    int ret = redisAsyncCommand(_redis_context, 
        CRedisPublisher::command_callback, this, "PUBLISH %s %s", 
        channel_name.c_str(), message.c_str());
    if (REDIS_ERR == ret)
    {
        printf("Publish command failed: %d\n", ret);
        return false;
    }

    return true;
}

void CRedisPublisher::connect_callback(const redisAsyncContext *redis_context,
    int status)
{
    if (status != REDIS_OK)
    {
        printf(": Error: %s\n", redis_context->errstr);
    }
    else
    {
        printf(": Redis connected!\n");
    }
}

void CRedisPublisher::disconnect_callback(
    const redisAsyncContext *redis_context, int status)
{
    if (status != REDIS_OK)
    {
        // 這裏異常退出,可以嘗試重連
        printf(": Error: %s\n", redis_context->errstr);
    }
}

// 消息接收回調函數
void CRedisPublisher::command_callback(redisAsyncContext *redis_context,
    void *reply, void *privdata)
{
    printf("command callback.\n");
    // 這裏不執行任何操作
}

void *CRedisPublisher::event_thread(void *data)
{
    if (NULL == data)
    {
        printf(": Error!\n");
        assert(false);
        return NULL;
    }

    CRedisPublisher *self_this = reinterpret_cast<CRedisPublisher *>(data);
    return self_this->event_proc();
}

void *CRedisPublisher::event_proc()
{
    sem_wait(_event_sem);

    // 開啓事件分發,event_base_dispatch會阻塞
    event_base_dispatch(_event_base);

    return NULL;
}

redis_subscriber.h

/*************************************************************************
    > File Name: redis_subscriber.h
    > Author: chenzengba
    > Mail: [email protected] 
    > Created Time: Sat 23 Apr 2016 10:15:09 PM CST
    > Description: 封裝hiredis,實現消息訂閱redis功能
 ***********************************************************************/

#ifndef REDIS_SUBSCRIBER_H
#define REDIS_SUBSCRIBER_H

#include <stdlib.h>
#include <hiredis/async.h>
#include <hiredis/adapters/libevent.h>
#include <string>
#include <vector>
#include <unistd.h>
#include <pthread.h>
#include <semaphore.h>
#include <boost/tr1/functional.hpp>

class CRedisSubscriber
{
public:
    typedef std::tr1::function<void (const char *, const char *, int)>     
    NotifyMessageFn;    
    // 回調函數對象類型,當接收到消息後調用回調把消息發送出去

    CRedisSubscriber();
    ~CRedisSubscriber();

    bool init(const NotifyMessageFn fn);    // 傳入回調對象
    bool uninit();
    bool connect();
    bool disconnect();

    // 可以多次調用,訂閱多個頻道
    bool subscribe(const std::string channel_name);

private:
    // 下面三個回調函數供redis服務調用
    // 連接回調
    static void connect_callback(const redisAsyncContext *redis_context,
        int status);

    // 斷開連接的回調
    static void disconnect_callback(const redisAsyncContext *redis_context,
        int status);

    // 執行命令回調
    static void command_callback(redisAsyncContext *redis_context,
        void *reply, void *privdata);

    // 事件分發線程函數
    static void *event_thread(void *data);
    void *event_proc();

private:
    // libevent事件對象
    event_base *_event_base;
    // 事件線程ID
    pthread_t _event_thread;
    // 事件線程的信號量
    sem_t _event_sem;
    // hiredis異步對象
    redisAsyncContext *_redis_context;

    // 通知外層的回調函數對象
    NotifyMessageFn _notify_message_fn;
};

#endif

redis_subscriber.cpp

/*************************************************************************
    > File Name: redis_subscriber.cpp
    > Author: chenzengba
    > Mail: [email protected] 
    > Created Time: Sat 23 Apr 2016 10:15:09 PM CST
    > Description: 
 ***********************************************************************/

#include <stddef.h>
#include <assert.h>
#include <string.h>
#include "redis_subscriber.h"

CRedisSubscriber::CRedisSubscriber():_event_base(0), _event_thread(0),
_redis_context(0)
{
}

CRedisSubscriber::~CRedisSubscriber()
{
}

bool CRedisSubscriber::init(const NotifyMessageFn fn)
{
    // initialize the event
    _notify_message_fn = fn;
    _event_base = event_base_new();    // 創建libevent對象
    if (NULL == _event_base)
    {
        printf(": Create redis event failed.\n");
        return false;
    }

    memset(_event_sem, 0, sizeof(_event_sem));
    int ret = sem_init(_event_sem, 0, 0);
    if (ret != 0)
    {
        printf(": Init sem failed.\n");
        return false;
    }

    return true;
}

bool CRedisSubscriber::uninit()
{
    _event_base = NULL;

    sem_destroy(_event_sem);   
    return true;
}

bool CRedisSubscriber::connect()
{
    // connect redis
    _redis_context = redisAsyncConnect("127.0.0.1", 6379);    // 異步連接到redis服務器上,使用默認端口
    if (NULL == _redis_context)
    {
        printf(": Connect redis failed.\n");
        return false;
    }

    if (_redis_context->err)
    {
        printf(": Connect redis error: %d, %s\n", 
            _redis_context->err, _redis_context->errstr);    // 輸出錯誤信息
        return false;
    }

    // attach the event
    redisLibeventAttach(_redis_context, _event_base);    // 將事件綁定到redis context上,使設置給redis的回調跟事件關聯

    // 創建事件處理線程
    int ret = pthread_create(_event_thread, 0, CRedisSubscriber::event_thread, this);
    if (ret != 0)
    {
        printf(": create event thread failed.\n");
        disconnect();
        return false;
    }

    // 設置連接回調,當異步調用連接後,服務器處理連接請求結束後調用,通知調用者連接的狀態
    redisAsyncSetConnectCallback(_redis_context, 
        CRedisSubscriber::connect_callback);

    // 設置斷開連接回調,當服務器斷開連接後,通知調用者連接斷開,調用者可以利用這個函數實現重連
    redisAsyncSetDisconnectCallback(_redis_context,
        CRedisSubscriber::disconnect_callback);

    // 啓動事件線程
    sem_post(_event_sem);
    return true;
}

bool CRedisSubscriber::disconnect()
{
    if (_redis_context)
    {
        redisAsyncDisconnect(_redis_context);
        redisAsyncFree(_redis_context);
        _redis_context = NULL;
    }

    return true;
}

bool CRedisSubscriber::subscribe(const std::string channel_name)
{
    int ret = redisAsyncCommand(_redis_context, 
        CRedisSubscriber::command_callback, this, "SUBSCRIBE %s", 
        channel_name.c_str());
    if (REDIS_ERR == ret)
    {
        printf("Subscribe command failed: %d\n", ret);
        return false;
    }

    printf(": Subscribe success: %s\n", channel_name.c_str());
    return true;
}

void CRedisSubscriber::connect_callback(const redisAsyncContext *redis_context,
    int status)
{
    if (status != REDIS_OK)
    {
        printf(": Error: %s\n", redis_context->errstr);
    }
    else
    {
        printf(": Redis connected!");
    }
}

void CRedisSubscriber::disconnect_callback(
    const redisAsyncContext *redis_context, int status)
{
    if (status != REDIS_OK)
    {
        // 這裏異常退出,可以嘗試重連
        printf(": Error: %s\n", redis_context->errstr);
    }
}

// 消息接收回調函數
void CRedisSubscriber::command_callback(redisAsyncContext *redis_context,
    void *reply, void *privdata)
{
    if (NULL == reply || NULL == privdata) {
        return ;
    }

    // 靜態函數中,要使用類的成員變量,把當前的this指針傳進來,用this指針間接訪問
    CRedisSubscriber *self_this = reinterpret_cast<CRedisSubscriber *>(privdata);
    redisReply *redis_reply = reinterpret_cast<redisReply *>(reply);

    // 訂閱接收到的消息是一個帶三元素的數組
    if (redis_reply->type == REDIS_REPLY_ARRAY  
    redis_reply->elements == 3)
    {
        printf(": Recieve message:%s:%d:%s:%d:%s:%d\n",
        redis_reply->element[0]->str, redis_reply->element[0]->len,
        redis_reply->element[1]->str, redis_reply->element[1]->len,
        redis_reply->element[2]->str, redis_reply->element[2]->len);

        // 調用函數對象把消息通知給外層
        self_this->_notify_message_fn(redis_reply->element[1]->str,
            redis_reply->element[2]->str, redis_reply->element[2]->len);
    }
}

void *CRedisSubscriber::event_thread(void *data)
{
    if (NULL == data)
    {
        printf(": Error!\n");
        assert(false);
        return NULL;
    }

CRedisSubscriber *self_this = reinterpret_cast<CRedisSubscriber *>(data);
    return self_this->event_proc();
}

void *CRedisSubscriber::event_proc()
{
    sem_wait(_event_sem);

    // 開啓事件分發,event_base_dispatch會阻塞
    event_base_dispatch(_event_base);

    return NULL;
}

問題1:hiredis官網沒有異步接口的實現例子。
hiredis提供了幾個異步通信的API,一開始根據API名字的理解,我們實現了跟redis服務器建立連接、訂閱和發佈的功能,可在實際使用的時候,程序並沒有像我們預想的那樣,除了能夠建立連接外,任何事情都沒發生。
網上查了很多資料,原來hiredis的異步實現是通過事件來分發redis發送過來的消息的,hiredis可以使用libae、libev、libuv和libevent中的任何一個實現事件的分發,網上的資料提示使用libae、libev和libuv可能發生其他問題,這裏爲了方便就選用libevent。hireds官網並沒有對libevent做任何介紹,也沒用說明使用異步機制需要引入事件的接口,所以一開始走了很多彎路。
關於libevent的使用這裏就不再贅述,詳情可以見libevent官網。
libevent官網:http://libevent.org/
ibevent api文檔:
https://www.monkey.org/~provos/libevent/doxygen-2.0.1/include_2event2_2event_8h.html#6e9827de8c3014417b11b48f2fe688ae

CRedisPublisher和CRedisSubscriber的初始化過程:
初始化事件處理,並獲得事件處理的實例:
_event_base = event_base_new();在獲得redisAsyncContext *之後,
調用redisLibeventAttach(_redis_context, _event_base);這樣就將事件處理和redis關聯起來,最後在另一個線程調用event_base_dispatch(_event_base);啓動事件的分發,這是一個阻塞函數,因此,創建了一個新的線程處理事件分發,值得注意的是,這裏用信號燈_event_sem控制線程的啓動,意在程序調用redisAsyncSetConnectCallback(_redis_context,CRedisSubscriber::connect_callback);
redisAsyncSetDisconnectCallback(_redis_context,CRedisSubscriber::disconnect_callback);之後,能夠完全捕捉到這兩個回調。

問題2 奇特的‘ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context’錯誤
有些人會覺得這兩個類設計有點冗餘,我們發現CRedisPublisher和CRedisSubscriber很多邏輯是一樣的,爲什麼不把他們整合到一起成一個類,既能夠發佈消息也能夠訂閱消息。其實一開始我就是這麼幹的,在使用的時候發現,用同個redisAsynContex *對象進行消息訂閱和發佈,與redis服務連接會自動斷開disconnect_callback回調會被調用,並且返回奇怪的錯誤:ERR only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context,因此,不能使用同個redisAsyncContext *對象實現發佈和訂閱。這裏爲了減少設計的複雜性,就將兩個類的邏輯分開了。
當然,你也可以將相同的邏輯抽象到一個基類裏,並實現publish和subscribe接口。

問題3 相關依賴的庫
編譯之前,需要安裝hiredis、libevent和boost庫,我是用的是Ubuntu x64系統。
hiredis官網:https://github.com/redis/hiredis
下載源碼解壓,進入解壓目錄,執行make make install命令。
libevent官網:http://libevent.org/下載最新的穩定版
解壓後進入解壓目錄,執行命令
./configure -prefix=/usr
sudo make make install
boost庫:直接執行安裝:sudo apt-get install libboost-dev
如果你不是用std::tr1::function的函數對象來給外層通知消息,就不需要boost庫。你可以用接口的形式實現回調,把接口傳給CRedisSubscribe類,讓它在接收到消息後調用接口回調,通知外層。

問題4 如何使用
最後貼出例子代碼。
publisher.cpp,實現發佈消息:

 /*************************************************************
  > File Name: publisher.cpp
  > Author: chenzengba
  > Mail: [email protected] 
  > Created Time: Sat 23 Apr 2016 12:13:24 PM CST
 ************************************************************/
//
#include "redis_publisher.h"

int main(int argc, char *argv[])
{
    CRedisPublisher publisher;

    bool ret = publisher.init();
    if (!ret) 
    {
        printf("Init failed.\n");
        return 0;
    }

    ret = publisher.connect();
    if (!ret)
    {
        printf("connect failed.");
        return 0;
    }

    while (true)
    {
        publisher.publish("test-channel", "Test message");
        sleep(1);
    }

    publisher.disconnect();
    publisher.uninit();
    return 0;
}

subscriber.cpp實現訂閱消息:


 /************************************************************
    > File Name: subscriber.cpp
    > Author: chenzengba
    > Mail: [email protected] 
    > Created Time: Sat 23 Apr 2016 12:26:42 PM CST
 *************************************************************/

#include "redis_subscriber.h"

void recieve_message(const char *channel_name,
    const char *message, int len)
{
  printf("Recieve message:\n channel name: %s\n message: %s\n",
        channel_name, message);
}

int main(int argc, char *argv[])
{
    CRedisSubscriber subscriber;
    CRedisSubscriber::NotifyMessageFn fn = 
        bind(recieve_message, std::tr1::placeholders::_1,
        std::tr1::placeholders::_2, 
        std::tr1::placeholders::_3);

    bool ret = subscriber.init(fn);
    if (!ret)
    {
        printf("Init failed.\n");
        return 0;
    }

    ret = subscriber.connect();
    if (!ret)
    {
        printf("Connect failed.\n");
        return 0;
    }

    subscriber.subscribe("test-channel");

    while (true)
    {
        sleep(1);
    }

    subscriber.disconnect();
    subscriber.uninit();

    return 0;
}

關於編譯的問題:在g++中編譯, 注意要加上-lhiredis -levent -lpthread參數 ,下面是一個簡單的Makefile:

EXE=server_main client_main
CC=g++
FLAG=-lhiredis -levent
OBJ=redis_publisher.o publisher.o redis_subscriber.o subscriber.o

all:$(EXE)

$(EXE):$(OBJ)
    $(CC) -o publisher redis_publisher.o publisher.o $(FLAG)
    $(CC) -o subscriber redis_subscriber.o subscriber.o $(FLAG)

redis_publisher.o:redis_publisher.h
redis_subscriber.o:redis_subscriber.h

publisher.o:publisher.cpp
    $(CC) -c publisher.cpp

subscriber.o:subscriber.cpp
    $(CC) -c subscriber.cpp
clean:
    rm publisher subscriber *.o

致謝:
redis異步API使用libevent:http://www.tuicool.com/articles/N73uuu

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章