hiredis是redis官方推荐的C/C++客户端代码库,使用hiredis库可以方便地进行redis数据库相关操作,大多数情况下采用同步调用的方式,下面通过一个简单例子对其工作流程进行介绍并对相关源码进行分析:
#include <stdio.h>
#include <string.h>
#include "hiredis.h"
int main()
{
redisContext *c = redisConnect("192.168.0.18", 9688);
if (c->err) { /* Error flags, 0 when there is no error */
printf("connect error.%s", c->errstr);
return 0;
}
else
{
printf("connected\n");
}
redisReply *reply = (redisReply *)redisCommand(c, "AUTH %s", "123456");
if (reply->type == REDIS_REPLY_ERROR) {
printf("Redis log in fail!\n");
}
else
{
printf("Redis log in success!\n");
}
freeReplyObject(reply);
char *value="It's a test";
redisReply *reply1 = redisCommand(c, "set key %s", value);
freeReplyObject(reply1);
redisReply *reply2 = redisCommand(c, "get key");
printf("key:1 value:%s\n", reply2->str);
freeReplyObject(reply2);
redisFree(c);
return 0;
}
首先调用redisConnect连接redis数据库,redisConnect基于tcp协议连接服务端,其中redisContext的定义如下:
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;
redisContext为表示一个连接的上下文结构体,包括ip、端口号、socket id和写缓存等信息,redisConnect代码如下:
redisContext *redisConnect(const char *ip, int port) {
redisContext *c;
c = redisContextInit();
if (c == NULL)
return NULL;
c->flags |= REDIS_BLOCK;
redisContextConnectTcp(c,ip,port,NULL);
return c;
}
可以看到首先调用 redisContextInit()初始化上下文、分配内存,之后调用redisContextConnectTcp(c,ip,port,NULL)通过tcp连接服务端。连接成功后通过redisCommand执行redis指令,代码如下:
void *redisCommand(redisContext *c, const char *format, ...) {
va_list ap;
va_start(ap,format);
void *reply = redisvCommand(c,format,ap);
va_end(ap);
return reply;
}
实际调用的是redisvCommand:
void *redisvCommand(redisContext *c, const char *format, va_list ap) {
if (redisvAppendCommand(c,format,ap) != REDIS_OK)
return NULL;
return __redisBlockForReply(c);
}
有两个函数调用,其中redisvAppendCommand负责解析指令并将指令转化为待发送的字节流存储到redisContext *c中的obuf中,随后__redisBlockForReply(c)负责通过tcp协议将命令字节流发送给服务端并接收服务端的回复,实际调用了函数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;
}
发送指令是通过调用redisBufferWrite(c,&wdone)完成的,其源代码为:
int redisBufferWrite(redisContext *c, int *done) {
int nwritten;
/* Return early when the context has seen an error. */
if (c->err)
return REDIS_ERR;
if (sdslen(c->obuf) > 0) {
nwritten = write(c->fd,c->obuf,sdslen(c->obuf));
if (nwritten == -1) {
if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) {
/* Try again later */
} else {
__redisSetError(c,REDIS_ERR_IO,NULL);
return REDIS_ERR;
}
} else if (nwritten > 0) {
if (nwritten == (signed)sdslen(c->obuf)) {
sdsfree(c->obuf);
c->obuf = sdsempty();
} else {
sdsrange(c->obuf,nwritten,-1);
}
}
}
if (done != NULL) *done = (sdslen(c->obuf) == 0);
return REDIS_OK;
}
可以看到最底层其实就是调用write函数对redisContext *c 的tcp socket fd进行连续的写操作来实现发送直到成员obuf的数据发送完成,同样通过redisBufferRead来接收服务端响应:
int redisBufferRead(redisContext *c) {
char buf[1024*16];
int nread;
/* Return early when the context has seen an error. */
if (c->err)
return REDIS_ERR;
nread = read(c->fd,buf,sizeof(buf));
if (nread == -1) {
if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) {
/* Try again later */
} else {
__redisSetError(c,REDIS_ERR_IO,NULL);
return REDIS_ERR;
}
} else if (nread == 0) {
__redisSetError(c,REDIS_ERR_EOF,"Server closed the connection");
return REDIS_ERR;
} else {
if (redisReaderFeed(c->reader,buf,nread) != REDIS_OK) {
__redisSetError(c,c->reader->err,c->reader->errstr);
return REDIS_ERR;
}
}
return REDIS_OK;
}
首先调用 read(c->fd,buf,sizeof(buf))将服务端发送的数据接收到buff中,随后调用redisReaderFeed(c->reader,buf,nread)将接收到的nread字节拷贝到c->reader的buffer缓存区,注意这里所有缓存操作都是通过简单动态字符串SDS数据结构实现的。redisGetReplyFromReader(c,&aux)实现对服务器返回消息的解析,并最终得到解析到的回复消息的结构体redisReply,并返回其指针。
经过分析可以看到,hiredis同步模式是典型单线程处理模式,单个线程首先发送命令,然后接受命令并进行解析。
这里需要注意在tcp连接建立中,默认客户端socket设定为c->flags |= REDIS_BLOCK;,即为阻塞的。
对于服务端数据的接收,在redisGetReply执行redisBufferRead和redisGetReplyFromReader两个函数的交替循环:
do {
if (redisBufferRead(c) == REDIS_ERR)
return REDIS_ERR;
if (redisGetReplyFromReader(c,&aux) == REDIS_ERR)
return REDIS_ERR;
} while (aux == NULL);
redisBufferRead函数中执行如下读操作:
int redisBufferRead(redisContext *c) {
char buf[1024*16];
int nread;
/* Return early when the context has seen an error. */
if (c->err)
return REDIS_ERR;
nread = read(c->fd,buf,sizeof(buf));
if (nread == -1) {
if ((errno == EAGAIN && !(c->flags & REDIS_BLOCK)) || (errno == EINTR)) {
/* Try again later */
} else {
__redisSetError(c,REDIS_ERR_IO,NULL);
return REDIS_ERR;
}
} else if (nread == 0) {
__redisSetError(c,REDIS_ERR_EOF,"Server closed the connection");
return REDIS_ERR;
} else {
if (redisReaderFeed(c->reader,buf,nread) != REDIS_OK) {
__redisSetError(c,c->reader->err,c->reader->errstr);
return REDIS_ERR;
}
}
return REDIS_OK;
}
如果读返回为-1的话,若非阻塞且返回错误为EAGAIN,则表示连续做read操作而没有数据可读,此时程序不会阻塞起来等待数据准备就绪返回,read函数会返回一个错误EAGAIN,提示你的应用程序现在没有数据可读请稍后再试。或者返回错误为EINTR,即进程在一个慢系统调用(slow system call)中阻塞系统调用被中断。这两种错误均不做处理,直接返回,其他错误则返回错误。当然这里由于默认是阻塞的,不会出现这两种情况。redisGetReplyFromReader负责解析指令,主要通过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;
}
直到解析指令成功才会退出循环。