学习总结一下官方发布的C版本客户端 hiredis,了解hiredis 客户端大致实现细节。在理解代码之间需要了解通信协议的特点,我上一篇转载的文章已经有过介绍,大家可以去看一下。

hiredis 提供了同步、异步访问,异步 API 需要与一些事件库协同工作,主要看一下同步API的实现。

hiredis 与服务端通信的API比较简单,主要有这几个步骤:

  • 建立连接
  • 发送命令
  • 等待结果并处理
  • 释放连接

一、相关数据结构

redisContext 保存连接建立后的上下文。 err 保存错误码,如果为0表示没错,如果非0那么错误说明保存在 errstr 中;fd是连接建立后的套接字;flags表示连接的标识;obuf 保存要向 redis-server 发送的命令内容;reader 用来读取从服务端返回的消息,redisReader中的buf成员用来保存内容;connection_type 表示连接类型,有两种分别是REDIS_CONN_TCPREDIS_CONN_UNIXtimeout 是连接时指定的超时时间。

/* Context for a connection to Redis */
typedef struct redisContext {
    int err; /* Error flags, 0 when there is no error */
    char errstr[128]; /* String representation of error when applicable */
    int fd;
    int flags;
    char *obuf; /* Write buffer */
    redisReader *reader; /* Protocol reader */

    enum redisConnectionType connection_type;
    struct timeval *timeout;

    struct {
        char *host;
        char *source_addr;
        int port;
    } tcp;

    struct {
        char *path;
    } unix_sock;

} redisContext;
/* This is the reply object returned by redisCommand() */
typedef struct redisReply {
    int type; /* REDIS_REPLY_* */
    long long integer; /* The integer when type is REDIS_REPLY_INTEGER */
    size_t len; /* Length of string */
    char *str; /* Used for both REDIS_REPLY_ERROR and REDIS_REPLY_STRING */
    size_t elements; /* number of elements, for REDIS_REPLY_ARRAY */
    struct redisReply **element; /* elements vector for REDIS_REPLY_ARRAY */
} redisReply;

redisReply 这个结构是保存发送命令后得到的返回结果,type 表示服务器返回结果的类型,比如 REDIS_REPLY_STRING,表示返回一个 string,此时内容就保存在 str 这个成员中,其他成员类似。有下面这几种类型:

#define REDIS_REPLY_STRING 1
#define REDIS_REPLY_ARRAY 2
#define REDIS_REPLY_INTEGER 3
#define REDIS_REPLY_NIL 4
#define REDIS_REPLY_STATUS 5
#define REDIS_REPLY_ERROR 6

(二)相关 api

建立连接

redisContext *redisConnect(const char *ip, int port);
redisContext *redisConnectWithTimeout(const char *ip, int port, const struct timeval tv);
redisContext *redisConnectNonBlock(const char *ip, int port);
redisContext *redisConnectBindNonBlock(const char *ip, int port,
                                       const char *source_addr);
redisContext *redisConnectBindNonBlockWithReuse(const char *ip, int port,
                                                const char *source_addr);
redisContext *redisConnectUnix(const char *path);
redisContext *redisConnectUnixWithTimeout(const char *path, const struct timeval tv);
redisContext *redisConnectUnixNonBlock(const char *path);
redisContext *redisConnectFd(int fd);

这些 api 都是建立连接的,可以根据需求或者条件选择合适的。连接建立成功返回 redisContext,将连接信息放到这个结构中,通过 err 成员来判断是否建立成功。

发送命令并等待结果

redisCommandredisAppendCommand 系列函数用来向服务器发送命令。

redisCommand -> redisvCommand -> redisvAppendCommand -> __redisAppendCommand

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;
}

这个函数组装发送的命令到redisContextobuf 中,obuf 可以动态扩容,所以不用担心溢出;接下来在函数 redisCommand 中调用 __redisBlockForReply,继而调用 redisGetReply 这个函数来获取返回结果。

int redisGetReply(redisContext *c, void **reply) {
    int wdone = 0;
    void *aux = NULL;

    /* Try to read pending replies */
    if (redisGetReplyFromReader(c,&aux) == REDIS_ERR)
        return REDIS_ERR;

    /* For the blocking context, flush output buffer and read reply */
    if (aux == NULL && c->flags & REDIS_BLOCK) {
        /* Write until done */
        do {
            if (redisBufferWrite(c,&wdone) == REDIS_ERR)
                return REDIS_ERR;
        } while (!wdone);

        /* Read until there is a reply */
        do {
            if (redisBufferRead(c) == REDIS_ERR)
                return REDIS_ERR;
            if (redisGetReplyFromReader(c,&aux) == REDIS_ERR)
                return REDIS_ERR;
        } while (aux == NULL);
    }

    /* Set reply object */
    if (reply != NULL) *reply = aux;
    return REDIS_OK;
}

这个函数分下面几步实现:
- 首先查看 redisReader 结构中的 buf 成员是否有数据,有的话则解析出 reply 并返回,否则进入下面;
- 把 obuf 缓冲区的数据全部写入 c->fd,写完后释放 obuf
- 接下来阻塞读取服务器返回的结果并将其放入redisContext -> redisReader -> buf 缓冲区中,进入下一步;
- 调用 redisGetReplyFromReader 解析 buf 中的数据,并返回reply;获取到的 redisReply 对象需要调用 freeReplyObject 显式释放,否则会泄漏。

redisAppendCommand 这个函数将发送的命令格式化放入 redisContextobuf 中,可以一次发送多条命令,然后接收一批结果,结果按照发送命令的顺序保存,这里利用了 pipeline 特性,可以减少网络传输的次数,提高IO吞吐量。

释放连接

使用完一个连接后需要调用 redisFree 函数来释放这个连接,主要是关闭套接字,释放申请的缓冲区。

void redisFree(redisContext *c) {
    if (c == NULL)
        return;
    if (c->fd > 0)
        close(c->fd);
    sdsfree(c->obuf);
    redisReaderFree(c->reader);
    free(c->tcp.host);
    free(c->tcp.source_addr);
    free(c->unix_sock.path);
    free(c->timeout);
    free(c);
}

一个例子

下面给出一个例子,对这几个API最基本的使用,测试的时候需要先安装 redis-server 并启动,默认端口为 6379,同时需要安装 hiredis 库,sudo yum install hiredis-devel,或者源码安装。

/*************************************************************************
    > File Name: redis-cli.c
    > Author: Tanswer_
    > Mail: 98duxm@gmail.com
    > Created Time: Thu Jun 28 15:49:43 2018
 ************************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <hiredis/hiredis.h>

int main()
{

    redisContext* c = redisConnect((char*)"127.0.0.1", 6379);
    if(c->err){
        redisFree(c);
        return 0;
    }
    printf("connect redis-server success.\n");

    const char* command = "set good luck";
    redisReply* r = (redisReply*)redisCommand(c, command);
    if(r == NULL){
        redisFree(c);
        return 0;
    }

    if(!(r->type == REDIS_REPLY_STATUS && strcasecmp(r->str, "OK") == 0)){
        printf("Failed to execute command[%s].\n", command);
        freeReplyObject(r);
        redisFree(c);
        return 0;
    }

    freeReplyObject(r);
    printf("Succeed to execute command[%s].\n", command);

    const char* command1 = "strlen good";
    r = (redisReply*)redisCommand(c, command1);
    if(r->type != REDIS_REPLY_INTEGER){
        printf("Failed to execute command[%s].\n", command1);
        freeReplyObject(r);
        redisFree(c);
        return 0;
    }

    int length = r -> integer;
    freeReplyObject(r);
    printf("The length of 'good' is %d.\n", length);
    printf("Succeed to execute command[%s].\n", command1);

    const char* command2 = "get good";
    r = (redisReply*)redisCommand(c, command2);
    if(r -> type != REDIS_REPLY_STRING){
        printf("Failed to execute command[%s].\n", command2);
        freeReplyObject(r);
        redisFree(c);
        return 0;
    }

    printf("The value of 'goo' is %s.\n", r->str);
    freeReplyObject(r);
    printf("Succeed to execute command[%s].\n", command2);

    redisFree(c);

    return 0;
}