Redis通过PUBLISH、SUBSCRIBE等命令实现了订阅与发布模式,该功能提供了两种机制:订阅/发布到频道 和 订阅/发布到模式。基本功能就是 通过频道/模式将消息发送到订阅了该频道或该模式的所有客户端去。
频道订阅与发布:服务器中结构中有频道订阅字典server.pubsub_channels,字典的键为被订阅的频道,值为一个链表,保存了订阅该频道的所有客户端。同时 ,客户端有一个保存该客户端所订阅的所有频道的链表client.pubsub_channels。
模式订阅与发布: 服务器中有一个模式链表server.pubsub_patterns,链表节点为一个由客户端与该客户端所订阅该模式 所组成的一个结构。 同时,在客户端结构中有一个模式链表cient.pubsub_patterns,保存了该客户端所订阅的所有模式。
频道的订阅与信息发布:
SUBSCRIBE/ PUBLISH命令:让客户端订阅任意数量的频道,每当有新信息发送到被订阅的频道时,信息就会被发送到所有订阅该频道的客户端。如下图所示:
其中客户端2、5、1分别订阅了频道1,当其他客户端发送信息到频道1时,该信息就会被发布到订阅该频道的所有客户端2、5、1。
订阅与发布函数如下:
订阅与退订:即将客户端加入服务器的频道订阅字典对应频道的链表中,同时在客户端的频道订阅字典中加入订阅的频道。而退订操作就是逆过程。
int pubsubSubscribeChannel(redisClient *c, robj *channel) {
dictEntry *de;
list *clients = NULL;
int retval = 0;
/* Add the channel to the client -> channels hash table */
// 将 channels 填接到 c->pubsub_channels 的集合中(值为 NULL 的字典视为集合)
if (dictAdd(c->pubsub_channels,channel,NULL) == DICT_OK) {
retval = 1;
incrRefCount(channel);
// 关联示意图
// {
// 频道名 订阅频道的客户端
// 'channel-a' : [c1, c2, c3],
// 'channel-b' : [c5, c2, c1],
// 'channel-c' : [c10, c2, c1]
// }
/* Add the client to the channel -> list of clients hash table */
// 在服务器中的频道订阅 pubsub_channels 字典中取出保存着所有订阅了 channel 的客户端的链表
// 如果 channel 不存在于字典,那么添加进去
de = dictFind(server.pubsub_channels,channel);
if (de == NULL) {
clients = listCreate();
dictAdd(server.pubsub_channels,channel,clients);
incrRefCount(channel);
} else {
clients = dictGetVal(de);
}
// before:
// 'channel' : [c1, c2]
// after:
// 'channel' : [c1, c2, c3]
// 将客户端添加到链表的末尾
listAddNodeTail(clients,c);
}
/* Notify the client */
// 回复客户端。
// 示例:
// redis 127.0.0.1:6379> SUBSCRIBE xxx
// Reading messages... (press Ctrl-C to quit)
// 1) "subscribe"
// 2) "xxx"
// 3) (integer) 1
addReply(c,shared.mbulkhdr[3]);
// "subscribe\n" 字符串
addReply(c,shared.subscribebulk);
// 被订阅的客户端
addReplyBulk(c,channel);
// 客户端订阅的频道和模式总数
addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns));
return retval;
}
/* Unsubscribe from all the channels. Return the number of channels the
* client was subscribed from.
*
* 退订客户端 c 订阅的所有频道。
*
* 返回被退订频道的总数。
*/
int pubsubUnsubscribeAllChannels(redisClient *c, int notify) {
// 频道迭代器
dictIterator *di = dictGetSafeIterator(c->pubsub_channels);
dictEntry *de;
int count = 0;
// 退订
while((de = dictNext(di)) != NULL) {
robj *channel = dictGetKey(de);
count += pubsubUnsubscribeChannel(c,channel,notify);
}
/* We were subscribed to nothing? Still reply to the client. */
// 如果在执行这个函数时,客户端没有订阅任何频道,
// 那么向客户端发送回复
if (notify && count == 0) {
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.unsubscribebulk);
addReply(c,shared.nullbulk);
addReplyLongLong(c,dictSize(c->pubsub_channels)+
listLength(c->pubsub_patterns));
}
dictReleaseIterator(di);
// 被退订的频道的数量
return count;
}
/* Publish a message
*
* 将 message 发送到所有订阅频道 channel 的客户端,
* 以及所有订阅了和 channel 频道匹配的模式的客户端。
*/
int pubsubPublishMessage(robj *channel, robj *message) {
int receivers = 0;
dictEntry *de;
listNode *ln;
listIter li;
/* Send to clients listening for that channel */
// 取出包含所有订阅频道 channel 的客户端的链表
// 并将消息发送给它们
de = dictFind(server.pubsub_channels,channel);server.pubsub_channels
if (de) { list *list = dictGetVal(de); listNode *ln; listIter li; // 遍历客户端链表,将 message 发送给它们 listRewind(list,&li); while ((ln = listNext(&li)) != NULL) { redisClient *c = ln->value; // 回复客户端。 // 示例: // 1) "message" // 2) "xxx" // 3) "hello" addReply(c,shared.mbulkhdr[3]); // "message" 字符串 addReply(c,shared.messagebulk); // 消息的来源频道 addReplyBulk(c,channel); // 消息内容 addReplyBulk(c,message); // 接收客户端计数 receivers++; } } /* Send to clients listening to matching channels */ // 将消息也发送给那些和频道匹配的模式 if (listLength(server.pubsub_patterns)) { // 遍历模式链表 listRewind(server.pubsub_patterns,&li); channel = getDecodedObject(channel); while ((ln = listNext(&li)) != NULL) { // 取出 pubsubPattern pubsubPattern *pat = ln->value; // 如果 channel 和 pattern 匹配 // 就给所有订阅该 pattern 的客户端发送消息 if (stringmatchlen((char*)pat->pattern->ptr, sdslen(pat->pattern->ptr), (char*)channel->ptr, sdslen(channel->ptr),0)) { // 回复客户端 // 示例: // 1) "pmessage" // 2) "*" // 3) "xxx" // 4) "hello" addReply(pat->client,shared.mbulkhdr[4]); addReply(pat->client,shared.pmessagebulk); addReplyBulk(pat->client,pat->pattern); addReplyBulk(pat->client,channel); addReplyBulk(pat->client,message); // 对接收消息的客户端进行计数 receivers++; } } decrRefCount(channel); } // 返回计数 return receivers;}
模式的订阅与发布:
如果某个模式与某频道匹配,则当有客户端向频道发布消息时,订阅了该模式的所有客户端也能接受到该消息。如图:
其中模式tweet.shop与频道tweet.shop.ipad和频道tweet.shop.kindle匹配,当有客户端向匹配的频道tweet.shop.kindle发送消息message时,订阅了模式weet.shop的客户端client23和client256都将接受到消息message。
模式的订阅退订:即在服务器的模式订阅链表中加入由该客户端和所订阅的模式构成的结构 pubsubPattern。同时在客户端的模式订阅链表中加入多订阅的模式。 而退订就是其逆过程。
/* Subscribe a client to a pattern. Returns 1 if the operation succeeded, or 0 if the client was already subscribed to that pattern.
*
* 设置客户端 c 订阅模式 pattern 。
*
* 订阅成功返回 1 ,如果客户端已经订阅了该模式,那么返回 0 。
*/
int pubsubSubscribePattern(redisClient *c, robj *pattern) {
int retval = 0;
// 在链表中查找模式,看客户端是否已经订阅了这个模式
if (listSearchKey(c->pubsub_patterns,pattern) == NULL) {
// 如果没有的话,执行以下代码
retval = 1;
pubsubPattern *pat;
// 将 pattern 添加到 c->pubsub_patterns 链表中
listAddNodeTail(c->pubsub_patterns,pattern);
incrRefCount(pattern);
// 创建并设置新的 pubsubPattern 结构
pat = zmalloc(sizeof(*pat));
pat->pattern = getDecodedObject(pattern);
pat->client = c;
// 添加到服务器模式订阅链表末尾
listAddNodeTail(server.pubsub_patterns,pat);
}
/* Notify the client */
// 回复客户端。
// 示例:
// redis 127.0.0.1:6379> PSUBSCRIBE xxx*
// Reading messages... (press Ctrl-C to quit)
// 1) "psubscribe"
// 2) "xxx*"
// 3) (integer) 1
addReply(c,shared.mbulkhdr[3]);
// 回复 "psubscribe" 字符串
addReply(c,shared.psubscribebulk);
// 回复被订阅的模式
addReplyBulk(c,pattern);
// 回复客户端订阅的频道和模式的总数
addReplyLongLong(c,dictSize(c->pubsub_channels)+listLength(c->pubsub_patterns));
return retval;
}
/* Unsubscribe from all the patterns. Return the number of patterns the
* client was subscribed from.
*
* 退订客户端 c 订阅的所有模式。
*
* 返回被退订模式的数量。
*/
int pubsubUnsubscribeAllPatterns(redisClient *c, int notify) {
listNode *ln;
listIter li;
int count = 0;
// 迭代客户端订阅模式的链表
listRewind(c->pubsub_patterns,&li);
while ((ln = listNext(&li)) != NULL) {
robj *pattern = ln->value;
// 退订,并计算退订数
count += pubsubUnsubscribePattern(c,pattern,notify);
}
// 如果在执行这个函数时,客户端没有订阅任何模式,
// 那么向客户端发送回复
if (notify && count == 0) {
/* We were subscribed to nothing? Still reply to the client. */
addReply(c,shared.mbulkhdr[3]);
addReply(c,shared.punsubscribebulk);
addReply(c,shared.nullbulk);
addReplyLongLong(c,dictSize(c->pubsub_channels)+
listLength(c->pubsub_patterns));
}
// 退订总数
return count;
}