Redis客户端是如何工作的?

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 <stdio.h>
#include <stdlib.h>

#include <hiredis.h>

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许可证

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容

  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,100评论 18 139
  • Redis使用的是自己构建的简单动态字符串(simple dynamic string,SDS)的抽象类型, 并将...
    但莫阅读 489评论 0 0
  • 1.1 资料 ,最好的入门小册子,可以先于一切文档之前看,免费。 作者Antirez的博客,Antirez维护的R...
    JefferyLcm阅读 16,967评论 1 51
  • 2017年是实新实施第四个三年规划的开局之年,本年度,我们将树立合作、开放、共享、共赢的发展新战略,继续把干净、有...
    秀以阅读 303评论 0 1
  • 黔北的初冬仍带有浓浓秋意。清晨的风微凉,穿透过玻璃窗的缝隙,悄悄地抚摸我熟睡的脸庞,轻轻地在我耳边哼唱秋日晨光协奏...
    黔中吟阅读 350评论 0 0