Redis自带的命令行客户端是“redis-cli”,支持redis的所有功能。例如,执行SET/GET操作:
$ src/redis-cli
127.0.0.1:6379> set mykey "Hello"
OK
127.0.0.1:6379> get mykey
"Hello"
“redis-cli”主体源码文件是src目录下的“redis-cli.c”,底层依赖于deps目录下的“hiredis”库。由于要支持Redis所有的功能,“redis-cli.c”的代码涉及太多的Redis私有概念,如“Latency、Slave、Pipe、Stat、Scan、LRU test、rpel”模式,以及集群操作等,对初学客户端实现不太友好。所以,我们剥离上层复杂的封装,直接看看“hiredis”库是如何与Redis服务端交互的。
Hiredis客户端库
hiredis的代码都在deps/hiredis目录下,约20个文件,代码量约5K行。主要分为同步、异步两种对外接口:
文件 | 描述 |
---|---|
hiredis.h | 同步接口 |
async.h | 异步接口 |
文件数和代码量都有点大,不同类型的接口大约各占一半的代码,所以按接口类型来拆分学习。先看看简单些的同步接口是如何实现的。
Hiredis同步接口
下面是官方自带的一个同步接口例子(代码有删减):
/* file: deps/hiredis/examples/example.c */
#include
#include
#include
int main(int argc, char **argv) {
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);
}
/* Set a key */
reply = redisCommand(c,"SET %s %s", "foo", "hello world");
printf("SET: %s\n", reply->str);
freeReplyObject(reply);
/* Try a GET */
reply = redisCommand(c,"GET foo");
printf("GET foo: %s\n", reply->str);
freeReplyObject(reply);
/* Disconnects and frees the context */
redisFree(c);
return 0;
}
上面的代码通过hiredis同步接口执行了SET/GET操作,编译执行看一下效果:
$ make hiredis-example
$ examples/hiredis-example
SET: OK
GET foo: hello world
除去连接和释放函数,最重要的就是同步执行命令的“ redisCommand”函数。以执行“GET foo”为例,来看一下它的具体实现:
/* file: hiredis.c */
void *redisCommand(redisContext *c, const char *format, ...) {
va_list ap;
void *reply = NULL;
va_start(ap,format);
reply = redisvCommand(c,format,ap);
va_end(ap);
return reply;
}
可变参数转成va_list,继续看“ redisvCommand”的实现:
/* file: hiredis.c */
void *redisvCommand(redisContext *c, const char *format, va_list ap) {
/* 将“GET foo”转换为Redis通信协议格式,保存到发送缓冲区 */
if (redisvAppendCommand(c,format,ap) != REDIS_OK)
return NULL;
/* 将发送缓冲区发送到网络,并阻塞接收回复 */
return __redisBlockForReply(c);
}
先看看是如何协议化,并保存到缓冲区的:
/* file: hiredis.c */
int redisvAppendCommand(redisContext *c, const char *format, va_list ap) {
char *cmd;
int len;
/* 将“GET foo”转换为Redis的RESP协议格式 */
len = redisvFormatCommand(&cmd,format,ap);
/* 略去协议化失败处理代码 */
…… ……
/* 将协议化后的数据保存到发送缓冲区 */
if (__redisAppendCommand(c,cmd,len) != REDIS_OK) {
free(cmd);
return REDIS_ERR;
}
free(cmd);
return REDIS_OK;
}
协议化函数“redisvFormatCommand”比较长,就不细看了。不过,Redis的RESP通信协议本身比较简单(可以参考前文“图解Redis通信协议”),我们知道“GET foo”会被转成“*2\r\n$3\r\nGET\r\n$3\r\nfoo\r\n”。
保存到缓冲区的函数比较简单:
int __redisAppendCommand(redisContext *c, const char *cmd, size_t len) {
sds newbuf;
/* 将输出缓冲区和当前的协议化数据拼接 */
newbuf = sdscatlen(c->obuf,cmd,len);
if (newbuf == NULL) {
__redisSetError(c,REDIS_ERR_OOM,"Out of memory");
return REDIS_ERR;
}
/* 更新缓冲区地址 */
c->obuf = newbuf;
return REDIS_OK;
}
sds是Redis私有的一种含有长度信息的字符串数据结构,sds具体实现可以参考“sds.h、sds.c”。“GET foo”已经被协议化,并保存在obuf中了,继续看看如何发送并接收回复。
static void *__redisBlockForReply(redisContext *c) {
void *reply;
if (c->flags & REDIS_BLOCK) {
if (redisGetReply(c,&reply) != REDIS_OK)
return NULL;
return reply;
}
return NULL;
}
状态判断,关键还是“redisGetReply”的实现:
int redisGetReply(redisContext *c, void **reply) {
int wdone = 0;
void *aux = NULL;
/* 略去读取残留回复代码 */
…… ……
/* For the blocking context, flush output buffer and read reply */
if (aux == NULL && c->flags & REDIS_BLOCK) {
/* Write until done */
do {
/* 将“c->obuf”的协议化数据发送到网络 */
if (redisBufferWrite(c,&wdone) == REDIS_ERR)
return REDIS_ERR;
} while (!wdone);
/* Read until there is a reply */
do {
/* 接收回复协议数据,并保存到“c->reader” */
if (redisBufferRead(c) == REDIS_ERR)
return REDIS_ERR;
/* 解析“c->reader”中的协议数据,获得回复信息 */
if (redisGetReplyFromReader(c,&aux) == REDIS_ERR)
return REDIS_ERR;
} while (aux == NULL);
}
/* Set reply object */
if (reply != NULL) *reply = aux;
return REDIS_OK;
}
收发数据的函数内部较为简单,“redisBufferWrite”最终调用了“write”把数据发送到网络,“ redisBufferRead”最终调用“read”把数据接收到“c->reader->buf”中。具体看看“redisGetReplyFromReader”的实现:
int redisGetReplyFromReader(redisContext *c, void **reply) {
if (redisReaderGetReply(c->reader,reply) == REDIS_ERR) {
__redisSetError(c,c->reader->err,c->reader->errstr);
return REDIS_ERR;
}
return REDIS_OK;
}
实际的解析在“ redisReaderGetReply”函数执行:
int redisReaderGetReply(redisReader *r, void **reply) {
/* 略去参数检查代码 */
…… ……
/* Set first item to process when the stack is empty. */
if (r->ridx == -1) {
/* 略去“r->rstack”初始化代码 */
…… ……
r->ridx = 0;
}
/* Process items in reply. */
while (r->ridx >= 0)
if (processItem(r) != REDIS_OK)
break;
/* 略去错误处理和reader缓冲区缩小代码 */
…… ……
/* Emit a reply when there is one. */
if (r->ridx == -1) {
if (reply != NULL)
*reply = r->reply;
r->reply = NULL;
}
return REDIS_OK;
}
具体的协议数据解析在“ processItem”函数中:
/* file: read.c */
static int processItem(redisReader *r) {
redisReadTask *cur = &(r->rstack[r->ridx]);
char *p;
/* check if we need to read type */
if (cur->type < 0) {
if ((p = readBytes(r,1)) != NULL) {
switch (p[0]) {
case '-':
cur->type = REDIS_REPLY_ERROR;
break;
case '+':
cur->type = REDIS_REPLY_STATUS;
break;
case ':':
cur->type = REDIS_REPLY_INTEGER;
break;
case '$':
cur->type = REDIS_REPLY_STRING;
break;
case '*':
cur->type = REDIS_REPLY_ARRAY;
break;
default:
__redisReaderSetErrorProtocolByte(r,*p);
return REDIS_ERR;
}
} else {
/* could not consume 1 byte */
return REDIS_ERR;
}
}
/* process typed item */
switch(cur->type) {
case REDIS_REPLY_ERROR:
case REDIS_REPLY_STATUS:
case REDIS_REPLY_INTEGER:
return processLineItem(r);
case REDIS_REPLY_STRING:
return processBulkItem(r);
case REDIS_REPLY_ARRAY:
return processMultiBulkItem(r);
default:
assert(NULL);
return REDIS_ERR; /* Avoid warning. */
}
}
在这个示例程序中,我们先是设置了key为“foo”的值是“hello world”,那么执行“GET foo”我们得到的协议数据应该是“$11\r\nhello world\r\n”。所以,这是一个“ REDIS_REPLY_STRING”类型的回复,并由“processBulkItem”函数来处理:
static int processBulkItem(redisReader *r) {
redisReadTask *cur = &(r->rstack[r->ridx]);
void *obj = NULL;
char *p, *s;
long len;
unsigned long bytelen;
int success = 0;
p = r->buf+r->pos;
s = seekNewline(p,r->len-r->pos);
if (s != NULL) {
p = r->buf+r->pos;
bytelen = s-(r->buf+r->pos)+2; /* include \r\n */
len = readLongLong(p); /* 读取长度 */
if (len < 0) {
/* 略去错误处理代码 */
…… ……
} else {
/* Only continue when the buffer contains the entire bulk item. */
bytelen += len+2; /* include \r\n */
if (r->pos+bytelen <= r->len) {
if (r->fn && r->fn->createString) /* 创建redisReply */
obj = r->fn->createString(cur,s+2,len);
else
obj = (void*)REDIS_REPLY_STRING;
success = 1;
}
}
/* Proceed when obj was created. */
if (success) {
if (obj == NULL) {
__redisReaderSetErrorOOM(r);
return REDIS_ERR;
}
r->pos += bytelen;
/* Set reply if this is the root object. */
if (r->ridx == 0) r->reply = obj;
moveToNextTask(r);
return REDIS_OK;
}
}
return REDIS_ERR;
}
从函数最后的“r->reply = obj;”可以知道,obj就只最终返回的redisReply结构,那么“obj = r->fn->createString(cur,s+2,len);”就是从协议数据解析出redisReply结构的函数了。这个函数指针是在连接的时候调用“redisReaderCreateWithFunctions(&defaultFunctions);”初始化的,实际执行的函数为“createStringObject”:
/* file: hiredis.c */
static void *createStringObject(const redisReadTask *task, char *str, size_t len) {
redisReply *r, *parent;
char *buf;
/* 创建redisReply结构 */
r = createReplyObject(task->type);
if (r == NULL)
return NULL;
buf = malloc(len+1);
if (buf == NULL) {
freeReplyObject(r);
return NULL;
}
/* 略去断言代码 */
…… ……
/* Copy string value */
memcpy(buf,str,len);
buf[len] = '\0';
r->str = buf; /* 将协议数据中的字符串赋值到redisReply结构的成员str */
r->len = len;
if (task->parent) {
parent = task->parent->obj;
assert(parent->type == REDIS_REPLY_ARRAY);
parent->element[task->idx] = r;
}
return r;
}
于是这个redisReply结构最终通过示例代码的reply结构,并被打印输出:
reply = redisCommand(c,"GET foo");
printf("SET: %s\n", reply->str);
总结
Hiredis同步接口两个关键点是:命令转换为协议数据、接收到的协议数据转换为redisReply结构。估计这两个部分,在异步接口中会重用,后续会分析一下Hiredis异步接口的实现。
参考
[1] Redis设计与实现,黄健宏(huangz)
[2] Redis 是如何处理命令的(客户端),Draveness
版权声明:自由转载-非商用-非衍生-保持署名(创意共享3.0许可证)