学习总结一下官方发布的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
(二)相关 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
成员来判断是否建立成功。
发送命令并等待结果
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 <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;
}