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

原文鏈接:https://blog.csdn.net/chenzba/article/details/51224715

 

轉自:https://blog.csdn.net/chenzba/article/details/51224715

 

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

       廢話不多說,先貼代碼。

 

redis_publisher.h


 
  1. /*************************************************************************

  2. > File Name: redis_publisher.h

  3. > Author: chenzengba

  4. > Mail: [email protected]

  5. > Created Time: Sat 23 Apr 2016 10:15:09 PM CST

  6. > Description: 封裝hiredis,實現消息發佈給redis功能

  7. ************************************************************************/

  8.  
  9. #ifndef REDIS_PUBLISHER_H

  10. #define REDIS_PUBLISHER_H

  11.  
  12. #include <stdlib.h>

  13. #include <hiredis/async.h>

  14. #include <hiredis/adapters/libevent.h>

  15. #include <string>

  16. #include <vector>

  17. #include <unistd.h>

  18. #include <pthread.h>

  19. #include <semaphore.h>

  20. #include <boost/tr1/functional.hpp>

  21.  
  22. class CRedisPublisher

  23. {

  24. public:

  25. CRedisPublisher();

  26. ~CRedisPublisher();

  27.  
  28. bool init();

  29. bool uninit();

  30. bool connect();

  31. bool disconnect();

  32.  
  33. bool publish(const std::string &channel_name,

  34. const std::string &message);

  35.  
  36. private:

  37. // 下面三個回調函數供redis服務調用

  38. // 連接回調

  39. static void connect_callback(const redisAsyncContext *redis_context,

  40. int status);

  41.  
  42. // 斷開連接的回調

  43. static void disconnect_callback(const redisAsyncContext *redis_context,

  44. int status);

  45.  
  46. // 執行命令回調

  47. static void command_callback(redisAsyncContext *redis_context,

  48. void *reply, void *privdata);

  49.  
  50. // 事件分發線程函數

  51. static void *event_thread(void *data);

  52. void *event_proc();

  53.  
  54. private:

  55. // libevent事件對象

  56. event_base *_event_base;

  57. // 事件線程ID

  58. pthread_t _event_thread;

  59. // 事件線程的信號量

  60. sem_t _event_sem;

  61. // hiredis異步對象

  62. redisAsyncContext *_redis_context;

  63. };

  64.  
  65. #endif

 

redis_publisher.cpp


 
  1. /*************************************************************************

  2. > File Name: redis_publisher.cpp

  3. > Author: chenzengba

  4. > Mail: [email protected]

  5. > Created Time: Sat 23 Apr 2016 10:15:09 PM CST

  6. > Description:

  7. ************************************************************************/

  8.  
  9. #include <stddef.h>

  10. #include <assert.h>

  11. #include <string.h>

  12. #include "redis_publisher.h"

  13.  
  14. CRedisPublisher::CRedisPublisher():_event_base(0), _event_thread(0),

  15. _redis_context(0)

  16. {

  17. }

  18.  
  19. CRedisPublisher::~CRedisPublisher()

  20. {

  21. }

  22.  
  23. bool CRedisPublisher::init()

  24. {

  25. // initialize the event

  26. _event_base = event_base_new(); // 創建libevent對象

  27. if (NULL == _event_base)

  28. {

  29. printf(": Create redis event failed.\n");

  30. return false;

  31. }

  32.  
  33. memset(&_event_sem, 0, sizeof(_event_sem));

  34. int ret = sem_init(&_event_sem, 0, 0);

  35. if (ret != 0)

  36. {

  37. printf(": Init sem failed.\n");

  38. return false;

  39. }

  40.  
  41. return true;

  42. }

  43.  
  44. bool CRedisPublisher::uninit()

  45. {

  46. _event_base = NULL;

  47.  
  48. sem_destroy(&_event_sem);

  49. return true;

  50. }

  51.  
  52. bool CRedisPublisher::connect()

  53. {

  54. // connect redis

  55. _redis_context = redisAsyncConnect("127.0.0.1", 6379); // 異步連接到redis服務器上,使用默認端口

  56. if (NULL == _redis_context)

  57. {

  58. printf(": Connect redis failed.\n");

  59. return false;

  60. }

  61.  
  62. if (_redis_context->err)

  63. {

  64. printf(": Connect redis error: %d, %s\n",

  65. _redis_context->err, _redis_context->errstr); // 輸出錯誤信息

  66. return false;

  67. }

  68.  
  69. // attach the event

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

  71.  
  72. // 創建事件處理線程

  73. int ret = pthread_create(&_event_thread, 0, &CRedisPublisher::event_thread, this);

  74. if (ret != 0)

  75. {

  76. printf(": create event thread failed.\n");

  77. disconnect();

  78. return false;

  79. }

  80.  
  81. // 設置連接回調,當異步調用連接後,服務器處理連接請求結束後調用,通知調用者連接的狀態

  82. redisAsyncSetConnectCallback(_redis_context,

  83. &CRedisPublisher::connect_callback);

  84.  
  85. // 設置斷開連接回調,當服務器斷開連接後,通知調用者連接斷開,調用者可以利用這個函數實現重連

  86. redisAsyncSetDisconnectCallback(_redis_context,

  87. &CRedisPublisher::disconnect_callback);

  88.  
  89. // 啓動事件線程

  90. sem_post(&_event_sem);

  91. return true;

  92. }

  93.  
  94. bool CRedisPublisher::disconnect()

  95. {

  96. if (_redis_context)

  97. {

  98. redisAsyncDisconnect(_redis_context);

  99. redisAsyncFree(_redis_context);

  100. _redis_context = NULL;

  101. }

  102.  
  103. return true;

  104. }

  105.  
  106. bool CRedisPublisher::publish(const std::string &channel_name,

  107. const std::string &message)

  108. {

  109. int ret = redisAsyncCommand(_redis_context,

  110. &CRedisPublisher::command_callback, this, "PUBLISH %s %s",

  111. channel_name.c_str(), message.c_str());

  112. if (REDIS_ERR == ret)

  113. {

  114. printf("Publish command failed: %d\n", ret);

  115. return false;

  116. }

  117.  
  118. return true;

  119. }

  120.  
  121. void CRedisPublisher::connect_callback(const redisAsyncContext *redis_context,

  122. int status)

  123. {

  124. if (status != REDIS_OK)

  125. {

  126. printf(": Error: %s\n", redis_context->errstr);

  127. }

  128. else

  129. {

  130. printf(": Redis connected!\n");

  131. }

  132. }

  133.  
  134. void CRedisPublisher::disconnect_callback(

  135. const redisAsyncContext *redis_context, int status)

  136. {

  137. if (status != REDIS_OK)

  138. {

  139. // 這裏異常退出,可以嘗試重連

  140. printf(": Error: %s\n", redis_context->errstr);

  141. }

  142. }

  143.  
  144. // 消息接收回調函數

  145. void CRedisPublisher::command_callback(redisAsyncContext *redis_context,

  146. void *reply, void *privdata)

  147. {

  148. printf("command callback.\n");

  149. // 這裏不執行任何操作

  150. }

  151.  
  152. void *CRedisPublisher::event_thread(void *data)

  153. {

  154. if (NULL == data)

  155. {

  156. printf(": Error!\n");

  157. assert(false);

  158. return NULL;

  159. }

  160.  
  161. CRedisPublisher *self_this = reinterpret_cast<CRedisPublisher *>(data);

  162. return self_this->event_proc();

  163. }

  164.  
  165. void *CRedisPublisher::event_proc()

  166. {

  167. sem_wait(&_event_sem);

  168.  
  169. // 開啓事件分發,event_base_dispatch會阻塞

  170. event_base_dispatch(_event_base);

  171.  
  172. return NULL;

  173. }

 

redis_subscriber.h


 
  1. /*************************************************************************

  2. > File Name: redis_subscriber.h

  3. > Author: chenzengba

  4. > Mail: [email protected]

  5. > Created Time: Sat 23 Apr 2016 10:15:09 PM CST

  6. > Description: 封裝hiredis,實現消息訂閱redis功能

  7. ************************************************************************/

  8.  
  9. #ifndef REDIS_SUBSCRIBER_H

  10. #define REDIS_SUBSCRIBER_H

  11.  
  12. #include <stdlib.h>

  13. #include <hiredis/async.h>

  14. #include <hiredis/adapters/libevent.h>

  15. #include <string>

  16. #include <vector>

  17. #include <unistd.h>

  18. #include <pthread.h>

  19. #include <semaphore.h>

  20. #include <boost/tr1/functional.hpp>

  21.  
  22. class CRedisSubscriber

  23. {

  24. public:

  25. typedef std::tr1::function<void (const char *, const char *, int)> \

  26. NotifyMessageFn; // 回調函數對象類型,當接收到消息後調用回調把消息發送出去

  27.  
  28. CRedisSubscriber();

  29. ~CRedisSubscriber();

  30.  
  31. bool init(const NotifyMessageFn &fn); // 傳入回調對象

  32. bool uninit();

  33. bool connect();

  34. bool disconnect();

  35.  
  36. // 可以多次調用,訂閱多個頻道

  37. bool subscribe(const std::string &channel_name);

  38.  
  39. private:

  40. // 下面三個回調函數供redis服務調用

  41. // 連接回調

  42. static void connect_callback(const redisAsyncContext *redis_context,

  43. int status);

  44.  
  45. // 斷開連接的回調

  46. static void disconnect_callback(const redisAsyncContext *redis_context,

  47. int status);

  48.  
  49. // 執行命令回調

  50. static void command_callback(redisAsyncContext *redis_context,

  51. void *reply, void *privdata);

  52.  
  53. // 事件分發線程函數

  54. static void *event_thread(void *data);

  55. void *event_proc();

  56.  
  57. private:

  58. // libevent事件對象

  59. event_base *_event_base;

  60. // 事件線程ID

  61. pthread_t _event_thread;

  62. // 事件線程的信號量

  63. sem_t _event_sem;

  64. // hiredis異步對象

  65. redisAsyncContext *_redis_context;

  66.  
  67. // 通知外層的回調函數對象

  68. NotifyMessageFn _notify_message_fn;

  69. };

  70.  
  71. #endif

 

redis_subscriber.cpp:


 
  1. /*************************************************************************

  2. > File Name: redis_subscriber.cpp

  3. > Author: chenzengba

  4. > Mail: [email protected]

  5. > Created Time: Sat 23 Apr 2016 10:15:09 PM CST

  6. > Description:

  7. ************************************************************************/

  8.  
  9. #include <stddef.h>

  10. #include <assert.h>

  11. #include <string.h>

  12. #include "redis_subscriber.h"

  13.  
  14. CRedisSubscriber::CRedisSubscriber():_event_base(0), _event_thread(0),

  15. _redis_context(0)

  16. {

  17. }

  18.  
  19. CRedisSubscriber::~CRedisSubscriber()

  20. {

  21. }

  22.  
  23. bool CRedisSubscriber::init(const NotifyMessageFn &fn)

  24. {

  25. // initialize the event

  26. _notify_message_fn = fn;

  27. _event_base = event_base_new(); // 創建libevent對象

  28. if (NULL == _event_base)

  29. {

  30. printf(": Create redis event failed.\n");

  31. return false;

  32. }

  33.  
  34. memset(&_event_sem, 0, sizeof(_event_sem));

  35. int ret = sem_init(&_event_sem, 0, 0);

  36. if (ret != 0)

  37. {

  38. printf(": Init sem failed.\n");

  39. return false;

  40. }

  41.  
  42. return true;

  43. }

  44.  
  45. bool CRedisSubscriber::uninit()

  46. {

  47. _event_base = NULL;

  48.  
  49. sem_destroy(&_event_sem);

  50. return true;

  51. }

  52.  
  53. bool CRedisSubscriber::connect()

  54. {

  55. // connect redis

  56. _redis_context = redisAsyncConnect("127.0.0.1", 6379); // 異步連接到redis服務器上,使用默認端口

  57. if (NULL == _redis_context)

  58. {

  59. printf(": Connect redis failed.\n");

  60. return false;

  61. }

  62.  
  63. if (_redis_context->err)

  64. {

  65. printf(": Connect redis error: %d, %s\n",

  66. _redis_context->err, _redis_context->errstr); // 輸出錯誤信息

  67. return false;

  68. }

  69.  
  70. // attach the event

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

  72.  
  73. // 創建事件處理線程

  74. int ret = pthread_create(&_event_thread, 0, &CRedisSubscriber::event_thread, this);

  75. if (ret != 0)

  76. {

  77. printf(": create event thread failed.\n");

  78. disconnect();

  79. return false;

  80. }

  81.  
  82. // 設置連接回調,當異步調用連接後,服務器處理連接請求結束後調用,通知調用者連接的狀態

  83. redisAsyncSetConnectCallback(_redis_context,

  84. &CRedisSubscriber::connect_callback);

  85.  
  86. // 設置斷開連接回調,當服務器斷開連接後,通知調用者連接斷開,調用者可以利用這個函數實現重連

  87. redisAsyncSetDisconnectCallback(_redis_context,

  88. &CRedisSubscriber::disconnect_callback);

  89.  
  90. // 啓動事件線程

  91. sem_post(&_event_sem);

  92. return true;

  93. }

  94.  
  95. bool CRedisSubscriber::disconnect()

  96. {

  97. if (_redis_context)

  98. {

  99. redisAsyncDisconnect(_redis_context);

  100. redisAsyncFree(_redis_context);

  101. _redis_context = NULL;

  102. }

  103.  
  104. return true;

  105. }

  106.  
  107. bool CRedisSubscriber::subscribe(const std::string &channel_name)

  108. {

  109. int ret = redisAsyncCommand(_redis_context,

  110. &CRedisSubscriber::command_callback, this, "SUBSCRIBE %s",

  111. channel_name.c_str());

  112. if (REDIS_ERR == ret)

  113. {

  114. printf("Subscribe command failed: %d\n", ret);

  115. return false;

  116. }

  117.  
  118. printf(": Subscribe success: %s\n", channel_name.c_str());

  119. return true;

  120. }

  121.  
  122. void CRedisSubscriber::connect_callback(const redisAsyncContext *redis_context,

  123. int status)

  124. {

  125. if (status != REDIS_OK)

  126. {

  127. printf(": Error: %s\n", redis_context->errstr);

  128. }

  129. else

  130. {

  131. printf(": Redis connected!");

  132. }

  133. }

  134.  
  135. void CRedisSubscriber::disconnect_callback(

  136. const redisAsyncContext *redis_context, int status)

  137. {

  138. if (status != REDIS_OK)

  139. {

  140. // 這裏異常退出,可以嘗試重連

  141. printf(": Error: %s\n", redis_context->errstr);

  142. }

  143. }

  144.  
  145. // 消息接收回調函數

  146. void CRedisSubscriber::command_callback(redisAsyncContext *redis_context,

  147. void *reply, void *privdata)

  148. {

  149. if (NULL == reply || NULL == privdata) {

  150. return ;

  151. }

  152.  
  153. // 靜態函數中,要使用類的成員變量,把當前的this指針傳進來,用this指針間接訪問

  154. CRedisSubscriber *self_this = reinterpret_cast<CRedisSubscriber *>(privdata);

  155. redisReply *redis_reply = reinterpret_cast<redisReply *>(reply);

  156.  
  157. // 訂閱接收到的消息是一個帶三元素的數組

  158. if (redis_reply->type == REDIS_REPLY_ARRAY &&

  159. redis_reply->elements == 3)

  160. {

  161. printf(": Recieve message:%s:%d:%s:%d:%s:%d\n",

  162. redis_reply->element[0]->str, redis_reply->element[0]->len,

  163. redis_reply->element[1]->str, redis_reply->element[1]->len,

  164. redis_reply->element[2]->str, redis_reply->element[2]->len);

  165.  
  166. // 調用函數對象把消息通知給外層

  167. self_this->_notify_message_fn(redis_reply->element[1]->str,

  168. redis_reply->element[2]->str, redis_reply->element[2]->len);

  169. }

  170. }

  171.  
  172. void *CRedisSubscriber::event_thread(void *data)

  173. {

  174. if (NULL == data)

  175. {

  176. printf(": Error!\n");

  177. assert(false);

  178. return NULL;

  179. }

  180.  
  181. CRedisSubscriber *self_this = reinterpret_cast<CRedisSubscriber *>(data);

  182. return self_this->event_proc();

  183. }

  184.  
  185. void *CRedisSubscriber::event_proc()

  186. {

  187. sem_wait(&_event_sem);

  188.  
  189. // 開啓事件分發,event_base_dispatch會阻塞

  190. event_base_dispatch(_event_base);

  191.  
  192. return NULL;

  193. }

 

問題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/

libevent 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控制線程的啓動,意在程序調用


 
  1. redisAsyncSetConnectCallback(_redis_context,

  2. &CRedisSubscriber::connect_callback);

  3. redisAsyncSetDisconnectCallback(_redis_context,

  4. &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,實現發佈消息:


 
  1. /*************************************************************************

  2. > File Name: publisher.cpp

  3. > Author: chenzengba

  4. > Mail: [email protected]

  5. > Created Time: Sat 23 Apr 2016 12:13:24 PM CST

  6. ************************************************************************/

  7.  
  8. #include "redis_publisher.h"

  9.  
  10. int main(int argc, char *argv[])

  11. {

  12. CRedisPublisher publisher;

  13.  
  14. bool ret = publisher.init();

  15. if (!ret)

  16. {

  17. printf("Init failed.\n");

  18. return 0;

  19. }

  20.  
  21. ret = publisher.connect();

  22. if (!ret)

  23. {

  24. printf("connect failed.");

  25. return 0;

  26. }

  27.  
  28. while (true)

  29. {

  30. publisher.publish("test-channel", "Test message");

  31. sleep(1);

  32. }

  33.  
  34. publisher.disconnect();

  35. publisher.uninit();

  36. return 0;

  37. }


subscriber.cpp實現訂閱消息:


 
  1. /*************************************************************************

  2. > File Name: subscriber.cpp

  3. > Author: chenzengba

  4. > Mail: [email protected]

  5. > Created Time: Sat 23 Apr 2016 12:26:42 PM CST

  6. ************************************************************************/

  7.  
  8. #include "redis_subscriber.h"

  9.  
  10. void recieve_message(const char *channel_name,

  11. const char *message, int len)

  12. {

  13. printf("Recieve message:\n channel name: %s\n message: %s\n",

  14. channel_name, message);

  15. }

  16.  
  17. int main(int argc, char *argv[])

  18. {

  19. CRedisSubscriber subscriber;

  20. CRedisSubscriber::NotifyMessageFn fn =

  21. bind(recieve_message, std::tr1::placeholders::_1,

  22. std::tr1::placeholders::_2, std::tr1::placeholders::_3);

  23.  
  24. bool ret = subscriber.init(fn);

  25. if (!ret)

  26. {

  27. printf("Init failed.\n");

  28. return 0;

  29. }

  30.  
  31. ret = subscriber.connect();

  32. if (!ret)

  33. {

  34. printf("Connect failed.\n");

  35. return 0;

  36. }

  37.  
  38. subscriber.subscribe("test-channel");

  39.  
  40. while (true)

  41. {

  42. sleep(1);

  43. }

  44.  
  45. subscriber.disconnect();

  46. subscriber.uninit();

  47.  
  48. return 0;

  49. }


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


 
  1. EXE=server_main client_main

  2. CC=g++

  3. FLAG=-lhiredis -levent

  4. OBJ=redis_publisher.o publisher.o redis_subscriber.o subscriber.o

  5.  
  6. all:$(EXE)

  7.  
  8. $(EXE):$(OBJ)

  9. $(CC) -o publisher redis_publisher.o publisher.o $(FLAG)

  10. $(CC) -o subscriber redis_subscriber.o subscriber.o $(FLAG)

  11.  
  12. redis_publisher.o:redis_publisher.h

  13. redis_subscriber.o:redis_subscriber.h

  14.  
  15. publisher.o:publisher.cpp

  16. $(CC) -c publisher.cpp

  17.  
  18. subscriber.o:subscriber.cpp

  19. $(CC) -c subscriber.cpp

  20. clean:

  21. rm publisher subscriber *.o

 

致謝:

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

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