Hiredis源码解析
Hiredis库主要包含三类API:同步api、异步api以及回复解析api。首先介绍一下同步api以及回复解析api。
1、同步api
1.1、建立tcp连接
函数原型:
redisContext *redisConnect(const char *ip, int port);
redisConnect函数用来创建一个上下文结构redisContext,并向reids服务器发起连接请求。源码如下所示:
redisContext *redisConnect(const char *ip, int port) { redisContext *c; c = redisContextInit(); if (c == NULL) return NULL; c->flags |= REDIS_BLOCK; redisContextConnectTcp(c,ip,port,NULL); return c; }
其中redisConext用来保存与redis服务器连接状态相关信息、输出缓冲区以及回复解析器。结构如下所示:
typedef struct redisContext { int err; char errstr[128]; int fd; int flags; char *obuf; redisReader *reader; ... } redisContext;
其中fd表示与redis服务器建立连接的socket描述符;而flag表示客户端标志位,表示客户端当前的状态;obuf用来保存输出缓存,用户调用reidsCommand向redis发送命令时,命令字符串首先会被追加到obuf中;reader是一个回复解析器,后续会介绍。
1.2 发送命令 & 接收回复
函数原型:
void *redisCommand(redisContext *c, const char *format, ...);
redisCommand函数返回NULL表示有错误发生,可以通过检查redisContext中的err得到错误类型;如果执行完成,则返回值是一个redisReply指针,包含了Redis的恢复信息。
redisCommand主要通过redisvCommand实现,而redisvCommand主要是通过redisvAppendCommand和__redisBlockForReply两个实现。
redisvAppendCommand源码如下所示:
int redisvAppendCommand(redisContext *c, const char *format, va_list ap) { ... len = redisvFormatCommand(&cmd,format,ap); if (len == -1) { __redisSetError(c,REDIS_ERR_OOM,"Out of memory"); return REDIS_ERR; } else if (len == -2) { __redisSetError(c,REDIS_ERR_OTHER,"Invalid format string"); return REDIS_ERR; } if (__redisAppendCommand(c,cmd,len) != REDIS_OK) { free(cmd); return REDIS_ERR; } ... }
redisvAppendCommand函数作用是解析用户的输入,并将用户输入的命令字符串转换成redis统一的格式,暂存到redisContext.obuf中。
__redisBlockForReply源码如下所示:
void *__redisBlockForReply(redisContext *c) { void *reply; if (c->flags & REDIS_BLOCK) { if (redisGetReply(c,&reply) != REDIS_OK) return NULL; return reply; } return NULL; } int redisGetReply(redisContext *c, void **reply) { if (redisGetReplyFromReader(c,&aux) == REDIS_ERR) return REDIS_ERR; if (aux == NULL && c->flags & REDIS_BLOCK) { do { if (redisBufferWrite(c,&wdone) == REDIS_ERR) return REDIS_ERR; } while (!wdone); do { if (redisBufferRead(c) == REDIS_ERR) return REDIS_ERR; if (redisGetReplyFromReader(c,&aux) == REDIS_ERR) return REDIS_ERR; } while (aux == NULL); } if (reply != NULL) *reply = aux; return REDIS_OK; }
redisGetReply中,首先是循环调用redisBufferWrite,将输出c->obuf中的所有内容发送给redis,然后循环调用redisBufferRead,读取redis的回复,调用redisGetReplyFromReader对回复信息进行解析。
redisBufferRead函数主要是从socket读取数据到buf中,然后通过函数redisReaderFeed,将bug内容追加到解析器的输入缓存中。
2、回复解析api
2.1、解析器缓存
解析器结构redisReader,源码如下所示:
typedef struct redisReader { char *buf; size_t pos; size_t len; size_t maxbuf; ... } redisReader;
buf就是输入缓存,redisReaderFeed函数把读取到的redis恢复信息都暂存于此;len表示当前缓存的容量;pos表示当前缓存的读取索引(每次读取输入缓存时,都从reader->buf + reader->pos处开始读取,读取数据之后,会增加pos的值);maxbuf表示输入缓存最大的允许闲置空间,当buf的空闲空间大于maxbuf时,就会buf,重新申请空间(默认值是16k)。
这里redisReaderFeed就是从socket中读取redis回复信息,追加到解析器缓存中。
2.2、解析回复信息
上述redisGetReply函数中,将redis回复信息追加到解析器输入缓存后,就会调用redisGetReplyFromReader对解析器的输入缓存进行信息解析,最终以redisReply结构呈现。源码如下所示:
typedef struct redisReply { int type; long long integer; int len; char *str; size_t elements; struct redisReply **element; } redisReply;
其中type表示redis回复信息的类型,其中基本类型主要有下面几种:
  • REDIS_REPLY_STATUS:状态回复,状态信息以'+'开头。str属性保存Redis回复的状态信息字符串,该字符串的长度保存在len属性中。
  • REDIS_REPLY_ERROR:错误回复,错误信息以'-'开头。str属性保存Redis回复的错误信息字符串,该字符串的长度保存在len属性中。
  • REDIS_REPLY_INTEGER:整数回复,整数信息以':'开头。integer 属性保存Redis回复的整数值。
  • REDIS_REPLY_STRING:单行字符串回复,这种信息以'$'开头。str属性保存Redis回复的字符串信息,该字符串的长度保存在len属性中。
  • REDIS_REPLY_NIL:Redis回复”nil”。而 REDIS_REPLY_ARRAY:数组回复,也就是嵌套回复,数组信息以'*'开头,后面数组元素个数。数组中的元素可以是任意类型。
调用redisReaderGetReply解析之后,最终会形成redisReply结构树,非叶子节点只能是REDIS_REPLY_ARRAY类型,叶子节点只能是上述提到的基本类型。
例如,redis回复信息是*3\r\n*3\r\n:11\r\n:12\r\n:13\r\n*3\r\n:21\r\n:22\r\n:23\r\n:31\r\n,那么最终形成的树如下所示:
使用redisReadTask任务结构来解析回复信息,构建每个redisReply结构节点,填充到树中合适的位置。源码如下所示:
typedef struct redisReader { ... redisReadTask rstack[9]; int ridx; void *reply; redisReplyObjectFunctions *fn; void *privdata; } redisReader;
redisReadTask结构数组rstak大小是9;其中rtask0表示redisReply结构树中的根节点;ridx表示当前处理第几层节点;fn包含了用于生成各种类型redisReply结构的函数;reply指向redisReply结构树中的根节点。
redisReadTask结构如下所示:
typedef struct redisReadTask { int type; int elements; int idx; void *obj; struct redisReadTask *parent; void *privdata; } redisReadTask;
这个地方有点绕
  • type表示redisReadTask结构当前处理的回复信息类型;
  • elements表示当前构建的REDIS_REPLY_ARRAY类型的redisReply结构节点中包含的子节点数目(上述redisReply结构节点中,数组element中的元素个数);
  • idx表示当前构建的redisReply结构节点,在其父节点redisReply中element数组中的索引;
  • obj指向当前正在构建的REDIS_REPLY_ARRAY类型的redisReply结构节点;
  • partent表示当前正在处理节点的父节点;
redisReaderGetReply源码如下所示:
int redisReaderGetReply(redisReader *r, void **reply) { if (reply != NULL) *reply = NULL; if (r->err) return REDIS_ERR; if (r->len == 0) return REDIS_OK; // 初始化操作 while (r->ridx >= 0) if (processItem(r) != REDIS_OK) break; if (r->err) return REDIS_ERR; if (r->pos >= 1024) { sdsrange(r->buf,r->pos,-1); r->pos = 0; r->len = sdslen(r->buf); } if (r->ridx == -1) { if (reply != NULL) *reply = r->reply; r->reply = NULL; } return REDIS_OK; }
回复解析主要步骤:
  1. 设置r->ridx为0,初始化r->rstack0,接下来开始构建根节点
  2. 循环调用processItem函数,直到r->ridx再次等于-1(深度优先),构建一棵redisReply结构树
processItem函数首先得到当前构建节点的结构redisReadTask *cur = &(r->rstack[r->ridx]),然后从输入缓存中读取首个字符,用来判断回复信息的类型,保存到cur->type中。
根据得到的回复类型信息,调用不同的函数处理不同的类型。这里重点看一下处理数组类型的processMultiBulkItem的实现逻辑:
static int processMultiBulkItem(redisReader *r) { redisReadTask *cur = &(r->rstack[r->ridx]); ... if (r->ridx == 8) return REDIS_ERR; if ((p = readLine(r,NULL)) != NULL) { elements = readLongLong(p); root = (r->ridx == 0); if (elements == -1) { if (r->fn && r->fn->createNil) obj = r->fn->createNil(cur); else obj = (void*)REDIS_REPLY_NIL; if (obj == NULL) { __redisReaderSetErrorOOM(r); return REDIS_ERR; } moveToNextTask(r); } else { if (r->fn && r->fn->createArray) obj = r->fn->createArray(cur,elements); else obj = (void*)REDIS_REPLY_ARRAY; if (obj == NULL) return REDIS_ERR; if (elements > 0) { cur->elements = elements; cur->obj = obj; r->ridx++; r->rstack[r->ridx].type = -1; r->rstack[r->ridx].elements = -1; r->rstack[r->ridx].idx = 0; r->rstack[r->ridx].obj = NULL; r->rstack[r->ridx].parent = cur; r->rstack[r->ridx].privdata = r->privdata; } else moveToNextTask(r); } if (root) r->reply = obj; return REDIS_OK; } return REDIS_ERR; }
首先得到当前构建节点的redisReadTask结构,调用readLine函数,解析出当前节点中包含的元素个数elements。
如果elements正确解析,调用r->fn->createArray创建一个数组类型的redisReply结构节点,将obj以及elements记录到cur中。
创建数组类型redisReply结构函数createArrayObject如下所示:
static void *createArrayObject(const redisReadTask *task, int elements) { ... r = createReplyObject(REDIS_REPLY_ARRAY); r->elements = elements; ... if (task->parent) { parent = task->parent->obj; assert(parent->type == REDIS_REPLY_ARRAY); parent->element[task->idx] = r; } return r; }
如果task->parent不为NULL,说明当前新建的redisReply结构节点有父节点,根据当前task得到该父节点redisReply结构parent,然后将当前节点保存到父节点element数组中的task->idx索引处。
数组类型的redisReply结构节点创建完成之后, 接下来就是构建各个子节点。首先就是将r->ridx加1(ridx为0是根节点),同时初始化r->rtaskr->ridx结构,其中r->rstackr->ridx.idx为0表示接下来首先构建第一个子节点。
如果elements等于0,调用moveToNextTask,为下一个要创建的节点找到合适的位置。源码如下所示:
static void moveToNextTask(redisReader *r) { redisReadTask *cur, *prv; while (r->ridx >= 0) { if (r->ridx == 0) { r->ridx--; return; } cur = &(r->rstack[r->ridx]); prv = &(r->rstack[r->ridx-1]); assert(prv->type == REDIS_REPLY_ARRAY); if (cur->idx == prv->elements-1) { r->ridx--; } else { assert(cur->idx < prv->elements); cur->type = -1; cur->elements = -1; cur->idx++; return; } } }
其中cur和prv分别表示当前正处理节点的redisReadTask结构以及父节点的redisReadTask结构。如果cur->idx小于prv->elements,那么接下来,cur结构就要开始构建当前节点的下一个兄弟节点,此时cur->idx需要加1;如果cur->idx等于prv->elements的话,说明当前节点,已经是父节点最后一个孩子节点了,那么接下来,就要开始构建当前节点的叔叔节点了,因此r->ridx--,表示上移一层,从处理父节点的rediReadTask结构开始,继续进行判断;如果当前处理的节点是根节点,即r->ridx=0,直接把r->ridx置为-1之后直接返回。
上面就是回复解析api主要的工作流程,这里redisReply结构树以及redisReadTask结构作用比较晦涩难懂(记住redisReply是最终的树结构,而redisReadTask只是用来辅助构建树结构)。
3、示例程序
示例程序可直接参考hiredis包中的example.c,本地启动一个redis-server测试即可。
#include <stdio.h> #include <stdlib.h> #include <string.h> #include "hiredis.h" int main(int argc, char **argv) { unsigned int j; redisContext *c; redisReply *reply; const char *hostname = (argc > 1) ? argv[1] : "127.0.0.1"; int port = (argc > 2) ? atoi(argv[2]) : 6379; struct timeval timeout = { 1, 500000 }; // 1.5 seconds c = redisConnectWithTimeout(hostname, port, timeout); if (c == NULL || c->err) { if (c) { printf("Connection error: %s\n", c->errstr); redisFree(c); } else { printf("Connection error: can't allocate redis context\n"); } exit(1); } /* PING server */ reply = static_cast<redisReply *>(redisCommand(c,"PING")); printf("PING: %s\n", reply->str); freeReplyObject(reply); /* Set a key */ reply = static_cast<redisReply *>(redisCommand(c,"SET %s %s", "foo", "hello world")); printf("SET: %s\n", reply->str); freeReplyObject(reply); ... /* Disconnects and frees the context */ redisFree(c); return 0; }
参考
https://github.com/redis/hiredis
http://yaocoder.blog.51cto.com/2668309/1297031
http://blog.csdn.net/kingqizhou/article/details/8104693

Hiredis源码阅读(一)相关推荐

  1. 应用监控CAT之cat-client源码阅读(一)

    CAT 由大众点评开发的,基于 Java 的实时应用监控平台,包括实时应用监控,业务监控.对于及时发现线上问题非常有用.(不知道大家有没有在用) 应用自然是最初级的,用完之后,还想了解下其背后的原理, ...

  2. centos下将vim配置为强大的源码阅读器

    每日杂事缠身,让自己在不断得烦扰之后终于有了自己的清静时光来熟悉一下我的工具,每次熟悉源码都需要先在windows端改好,拖到linux端,再编译.出现问题,还得重新回到windows端,这个过程太耗 ...

  3. 源码阅读:AFNetworking(十六)——UIWebView+AFNetworking

    该文章阅读的AFNetworking的版本为3.2.0. 这个分类提供了对请求周期进行控制的方法,包括进度监控.成功和失败的回调. 1.接口文件 1.1.属性 /**网络会话管理者对象*/ @prop ...

  4. 源码阅读:SDWebImage(六)——SDWebImageCoderHelper

    该文章阅读的SDWebImage的版本为4.3.3. 这个类提供了四个方法,这四个方法可分为两类,一类是动图处理,一类是图像方向处理. 1.私有函数 先来看一下这个类里的两个函数 /**这个函数是计算 ...

  5. mybatis源码阅读

    说下mybatis执行一个sql语句的流程 执行语句,事务等SqlSession都交给了excutor,excutor又委托给statementHandler SimpleExecutor:每执行一次 ...

  6. 24 UsageEnvironment使用环境抽象基类——Live555源码阅读(三)UsageEnvironment

    24 UsageEnvironment使用环境抽象基类--Live555源码阅读(三)UsageEnvironment 24 UsageEnvironment使用环境抽象基类--Live555源码阅读 ...

  7. Transformers包tokenizer.encode()方法源码阅读笔记

    Transformers包tokenizer.encode()方法源码阅读笔记_天才小呵呵的博客-CSDN博客_tokenizer.encode

  8. 源码阅读笔记 BiLSTM+CRF做NER任务 流程图

    源码阅读笔记 BiLSTM+CRF做NER任务(二) 源码地址:https://github.com/ZhixiuYe/NER-pytorch 本篇正式进入源码的阅读,按照流程顺序,一一解剖. 一.流 ...

  9. 源码阅读:AFNetworking(八)——AFAutoPurgingImageCache

    该文章阅读的AFNetworking的版本为3.2.0. AFAutoPurgingImageCache该类是用来管理内存中图片的缓存. 1.接口文件 1.1.AFImageCache协议 这个协议定 ...

最新文章

  1. nsqjs客户端的部署
  2. RocketMQ集群启动报错:java.lang.RuntimeException: Lock failed,MQ already started
  3. 前端怎么画三角形_前端小技巧:边框写三角形
  4. nginx 与php版本,nginx-php不同版本问题
  5. c++ error函数_Linux中create_elf_tables函数整型溢出漏洞分析(CVE201814634)
  6. 汽车燃料效率分析实例
  7. ubuntu之解决安装python3.6.4后出现error while loading shared libraries: libpython3.6m.so.1.0的问题
  8. python有趣小程序-Python全栈开发-有趣的小程序
  9. Swift和R3联手了,跨境支付市场竞争升级
  10. 【转】eclipse 查看原始类出现The jar file rt.jar has no source attachment解决方法
  11. 电脑常识——更改鼠标光标(另附一套MC指针)
  12. 网络安全事件收集,分析
  13. JAX-RS 从傻逼到牛叉 2:开发一个简单的服务
  14. iMeta: 整合宏组学重新认识生命和环境科学
  15. Oracle查询数据表数据很少却很慢
  16. ubuntu 18.04 下安装微信
  17. 【HTML 教程】iframe
  18. cadence——基本操作1
  19. Radius/Free Radius/Diameter协议
  20. 如何在Python中进行换行(换行)?

热门文章

  1. Python 安装模块(自用)
  2. 精细化营销推广渠道分析
  3. ? Emoji ? - 收藏集 - 掘金
  4. java的青蛙跳井_青蛙是如何跳出井口的
  5. 知识付费如何二开分销功能
  6. 【EasyDL Pro】中草药材AI识别师
  7. 音乐播放器通知栏切歌(Notification+BroadcastReceiver)
  8. 会议论文小套路:解读交通管科类优秀会议论文的文章结构
  9. 使用JS代码禁止某些地区用户访问网站
  10. 国内云视频会议领域第一股 让理想照进现实!