本文共 6587 字,大约阅读时间需要 21 分钟。
学习总结一下官方发布的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_TCP
和 REDIS_CONN_UNIX
;timeout
是连接时指定的超时时间。
/* 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
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
成员来判断是否建立成功。
redisCommand
和 redisAppendCommand
系列函数用来向服务器发送命令。
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;}
这个函数组装发送的命令到redisContext
的 obuf
中,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
这个函数将发送的命令格式化放入 redisContext
的 obuf
中,可以一次发送多条命令,然后接收一批结果,结果按照发送命令的顺序保存,这里利用了 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#include #include #include 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;}