一 序
上一篇整理了redis的server端流程,本篇文章介绍的是 Redis 客户端如何处理输入的命令、向服务发送命令以及取得服务端回复并输出到终端等过程。 简单的交互过程如下:
一、Client 发起socket 连接
二、Server 接受socket连接
三、客户端 开始写入
四、server 端接收写入
五、server 返回写入结果
六、Client收到返回结果
详细调用是大图镇楼:图片来自,https://www.jianshu.com/p/9ed98c7cbe6b
相关知识点:Linux Socket 建立流程和epoll I/O 多路复用技术。
先看下客户端的相关属性。源码在redis-cli.c
static redisContext *context;(hiredis.h 用来保存与redis服务器连接状态相关信息、输出缓冲区以及回复解析器)
static struct config (源码在redis_cli.c)会初始化 config 全局变量,该变量记录了客户端几乎所有的配置参数信息,
而 context 用于连接 redis 服务器。看一下 config 的结构.
static struct config {
char *hostip;
int hostport;
char *hostsocket;
long repeat;
long interval;
int dbnum;
int interactive;
int shutdown;
int monitor_mode;
int pubsub_mode;
int latency_mode;
int latency_dist_mode;
int latency_history;
int lru_test_mode;
long long lru_test_sample_size;
int cluster_mode;
int cluster_reissue_command;
int slave_mode;
int pipe_mode;
int pipe_timeout;
int getrdb_mode;
int stat_mode;
int scan_mode;
int intrinsic_latency_mode;
int intrinsic_latency_duration;
char *pattern;
char *rdb_filename;
int bigkeys;
int stdinarg; /* get last arg from stdin. (-x option) */
char *auth;
int output; /* output mode, see OUTPUT_* defines */
sds mb_delim;
char prompt[128];
char *eval;
int eval_ldb;
int eval_ldb_sync; /* Ask for synchronous mode of the Lua debugger. */
int eval_ldb_end; /* Lua debugging session ended. */
int enable_ldb_on_eval; /* Handle manual SCRIPT DEBUG + EVAL commands. */
int last_cmd_type;
} config;
redisContext 源码在(hiredis.h)
/* 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 */
} redisContext;
二 、Client 发起socket 连接
我们也从main开始,
int main(int argc, char **argv) {
int firstarg;
config.hostip = sdsnew("127.0.0.1");
config.hostport = 6379;
config.hostsocket = NULL;
config.repeat = 1;
config.interval = 0;
config.dbnum = 0;
config.interactive = 0;
config.shutdown = 0;
config.monitor_mode = 0;
config.pubsub_mode = 0;
config.latency_mode = 0;
config.latency_dist_mode = 0;
config.latency_history = 0;
config.lru_test_mode = 0;
config.lru_test_sample_size = 0;
config.cluster_mode = 0;
config.slave_mode = 0;
config.getrdb_mode = 0;
config.stat_mode = 0;
config.scan_mode = 0;
config.intrinsic_latency_mode = 0;
config.pattern = NULL;
config.rdb_filename = NULL;
config.pipe_mode = 0;
config.pipe_timeout = REDIS_CLI_DEFAULT_PIPE_TIMEOUT;
config.bigkeys = 0;
config.stdinarg = 0;
config.auth = NULL;
config.eval = NULL;
config.eval_ldb = 0;
config.eval_ldb_end = 0;
config.eval_ldb_sync = 0;
config.enable_ldb_on_eval = 0;
config.last_cmd_type = -1;
pref.hints = 1;
spectrum_palette = spectrum_palette_color;
spectrum_palette_size = spectrum_palette_color_size;
if (!isatty(fileno(stdout)) && (getenv("FAKETTY") == NULL))
config.output = OUTPUT_RAW;
else
config.output = OUTPUT_STANDARD;
config.mb_delim = sdsnew("\n");
firstarg = parseOptions(argc,argv);
argc -= firstarg;
argv += firstarg;
/* Latency mode */
if (config.latency_mode) {
if (cliConnect(0) == REDIS_ERR) exit(1);
latencyMode();
}
/* Latency distribution mode */
if (config.latency_dist_mode) {
if (cliConnect(0) == REDIS_ERR) exit(1);
latencyDistMode();
}
/* Slave mode */
if (config.slave_mode) {
if (cliConnect(0) == REDIS_ERR) exit(1);
slaveMode();
}
/* Get RDB mode. */
if (config.getrdb_mode) {
if (cliConnect(0) == REDIS_ERR) exit(1);
getRDB();
}
/* Pipe mode */
if (config.pipe_mode) {
if (cliConnect(0) == REDIS_ERR) exit(1);
pipeMode();
}
/* Find big keys */
if (config.bigkeys) {
if (cliConnect(0) == REDIS_ERR) exit(1);
findBigKeys();
}
/* Stat mode */
if (config.stat_mode) {
if (cliConnect(0) == REDIS_ERR) exit(1);
if (config.interval == 0) config.interval = 1000000;
statMode();
}
/* Scan mode */
if (config.scan_mode) {
if (cliConnect(0) == REDIS_ERR) exit(1);
scanMode();
}
/* LRU test mode */
if (config.lru_test_mode) {
if (cliConnect(0) == REDIS_ERR) exit(1);
LRUTestMode();
}
/* Intrinsic latency mode */
if (config.intrinsic_latency_mode) intrinsicLatencyMode();
/* Start interactive mode when no command is provided */
if (argc == 0 && !config.eval) {
/* Ignore SIGPIPE in interactive mode to force a reconnect */
signal(SIGPIPE, SIG_IGN);
/* Note that in repl mode we don't abort on connection error.
* A new attempt will be performed for every command send. */
cliConnect(0);
repl();
}
/* Otherwise, we have some arguments to execute */
if (cliConnect(0) != REDIS_OK) exit(1);
if (config.eval) {
return evalMode(argc,argv);
} else {
return noninteractive(argc,convertToSds(argc,argv));
}
}
可以看到Redis 中有好多模式,包括:Latency、Slave、Pipe、Stat、Scan、LRU test 等等模式,不过这些模式都不是这篇文章关注的重点,我们只会关注最常见的 repl 模式。主要的函数有:
cliConnect(0);
repl();
/* Connect to the server. If force is not zero the connection is performed
* even if there is already a connected socket. */
//连接,非0为强制执行
static int cliConnect(int force) {
if (context == NULL || force) {
if (context != NULL) {
redisFree(context);
}
if (config.hostsocket == NULL) {//TCP Socket连接
context = redisConnect(config.hostip,config.hostport);
} else {//Unix Socket连接
context = redisConnectUnix(config.hostsocket);
}
if (context->err) {//连接失败
fprintf(stderr,"Could not connect to Redis at ");
if (config.hostsocket == NULL)
fprintf(stderr,"%s:%d: %s\n",config.hostip,config.hostport,context->errstr);
else
fprintf(stderr,"%s: %s\n",config.hostsocket,context->errstr);
redisFree(context);
context = NULL;
return REDIS_ERR;
}
/* Set aggressive KEEP_ALIVE socket option in the Redis context socket
* in order to prevent timeouts caused by the execution of long
* commands. At the same time this improves the detection of real
* errors. */
anetKeepAlive(NULL, context->fd, REDIS_CLI_KEEPALIVE_INTERVAL);
/* Do AUTH and select the right DB. */
//认证
if (cliAuth() != REDIS_OK)
return REDIS_ERR;
//选择db
if (cliSelect() != REDIS_OK)
return REDIS_ERR;
}
return REDIS_OK;
}
cliConnect主要包含redisConnect、redisConnectUnix方法。这两个方法分别用于TCP Socket连接以及Unix Socket连接,Unix Socket用于同一主机进程间的通信。我们上面是采用的TCP Socket连接方式也就是我们平常生产环境常用的方式,这里不讨论Unix Socket连接方式,当然cliConnect方法中还会调用cliAuth方法用于权限验证、cliSelect用于db选择,这里不展开讨论。
看下redisConnect,源码在hiRedis.c
/* Connect to a Redis instance. On error the field error in the returned
* context will be set to the return value of the error function.
* When no set of reply functions is given, the default set will be used. */
//连接redis,参数是ip跟端口号
redisContext *redisConnect(const char *ip, int port) {
redisContext *c;
//创建redisContext
c = redisContextInit();
if (c == NULL)
return NULL;
c->flags |= REDIS_BLOCK;
//向reids服务器发起连接请求
redisContextConnectTcp(c,ip,port,NULL);
return c;
}
具体实现调用了redisContextConnectTcp(),源码在net.c,开始获取IP地址和端口用于建立连接,主要方法如下:
static int _redisContextConnectTcp(redisContext *c, const char *addr, int port,
const struct timeval *timeout,
const char *source_addr) {
...
s = socket(p->ai_family,p->ai_socktype,p->ai_protocol
connect(s,p->ai_addr,p->ai_addrlen)
...
}
到此客户端向服务端发起建立socket连接,并且等待服务器端响应。
当服务器连接成功时,context 的 fd 为连接成功后的 sockfd,flags 设置为 REDIS_CONNECTED,
三 Server 接受socket连接
服务器接收客户端的请求首先是从epoll_wait取出相关的事件,然后执行acceptTcpHandler或者acceptUnixHandler方法,那么这两个方法对应的事件是在什么时候注册的呢?他们是在服务器端初始化的时候创建。具体参见:https://blog.csdn.net/bohu83/article/details/85009255
因为不是客户端的主要流程,本篇不展开。
四 客户端开始写入
客户端在与服务器端建立好socket连接之后,进入交互模式,看下函数repl(),源码在redis_cli.c。
static void repl(void) {
sds historyfile = NULL;
int history = 0;
char *line;
int argc;
sds *argv;
/* Initialize the help and, if possible, use the COMMAND command in order
* to retrieve missing entries. */
cliInitHelp();
cliIntegrateHelp();
config.interactive = 1;
linenoiseSetMultiLine(1);
linenoiseSetCompletionCallback(completionCallback);
linenoiseSetHintsCallback(hintsCallback);
linenoiseSetFreeHintsCallback(freeHintsCallback);
/* Only use history and load the rc file when stdin is a tty. */
if (isatty(fileno(stdin))) {
historyfile = getDotfilePath(REDIS_CLI_HISTFILE_ENV,REDIS_CLI_HISTFILE_DEFAULT);
if (historyfile != NULL) {
history = 1;
linenoiseHistoryLoad(historyfile);
}
cliLoadPreferences();
}
cliRefreshPrompt();
while((line = linenoise(context ? config.prompt : "not connected> ")) != NULL) {
if (line[0] != '\0') {
argv = cliSplitArgs(line,&argc);
if (history) linenoiseHistoryAdd(line);
if (historyfile) linenoiseHistorySave(historyfile);
if (argv == NULL) {
printf("Invalid argument(s)\n");
linenoiseFree(line);
continue;
} else if (argc > 0) {
if (strcasecmp(argv[0],"quit") == 0 ||
strcasecmp(argv[0],"exit") == 0)
{
exit(0);
} else if (argv[0][0] == ':') {
cliSetPreferences(argv,argc,1);
continue;
} else if (strcasecmp(argv[0],"restart") == 0) {
if (config.eval) {
config.eval_ldb = 1;
config.output = OUTPUT_RAW;
return; /* Return to evalMode to restart the session. */
} else {
printf("Use 'restart' only in Lua debugging mode.");
}
} else if (argc == 3 && !strcasecmp(argv[0],"connect")) {
sdsfree(config.hostip);
config.hostip = sdsnew(argv[1]);
config.hostport = atoi(argv[2]);
cliRefreshPrompt();
cliConnect(1);
} else if (argc == 1 && !strcasecmp(argv[0],"clear")) {
linenoiseClearScreen();
} else {
long long start_time = mstime(), elapsed;
int repeat, skipargs = 0;
char *endptr;
repeat = strtol(argv[0], &endptr, 10);
if (argc > 1 && *endptr == '\0' && repeat) {
skipargs = 1;
} else {
repeat = 1;
}
issueCommandRepeat(argc-skipargs, argv+skipargs, repeat);
/* If our debugging session ended, show the EVAL final
* reply. */
if (config.eval_ldb_end) {
config.eval_ldb_end = 0;
cliReadReply(0);
printf("\n(Lua debugging session ended%s)\n\n",
config.eval_ldb_sync ? "" :
" -- dataset changes rolled back");
}
elapsed = mstime()-start_time;
if (elapsed >= 500 &&
config.output == OUTPUT_STANDARD)
{
printf("(%.2fs)\n",(double)elapsed/1000);
}
}
}
/* Free the argument vector */
sdsfreesplitres(argv,argc);
}
/* linenoise() returns malloc-ed lines like readline() */
linenoiseFree(line);
}
exit(0);
}
可以看到 linenoise
方法的调用,通过其中的 prompt
和 not connected>
可以判断出,这里向终端中输出了提示符,同时会调用 fgets
从标准输入中读取字符串. linenoise 是一款优秀的命令行编辑库,被广泛的运用在各种DB上.细节不展开。看下主要流程。
贴一下整理的调用关系,图片会更清晰:
main-->repl()-->linenoise( issueCommandRepeat )--> cliSendCommand()
-->redisAppendCommandArgv(hiredis.c)-->redisFormatCommandArgv()
-->__redisAppendCommand()
-->cliReadReply(redis-cli.c)--->redisGetReply(hiredis.c)-->redisGetReply()-->redisBufferWrite(c->fd)
cliSendCommand方法会判断命令是否为特殊命令,如:help\shutdown等。客户端会根据以上命令设置对应的输出格式以及客户端的模式,因为这里我们是普通写入,所以不会涉及到以上的情况。
cliSendCommand方法会调用redisAppendCommandArgv方法,redisAppendCommandArgv方法会调用redisFormatCommandArgv和__redisAppendCommand方法。redisFormatCommandArgv方法用于将客户端输入的内容格式化成redis协议:
Redis 客户端与 Redis 服务进行通讯时,会使用名为 RESP(REdis Serialization Protocol) 的协议,它的使用非常简单,并且可以序列化多种数据类型包括整数、字符串以及数组等。
对于 RESP 协议的详细介绍可以看官方文档中的 Redis Protocol specification,在这里对这个协议进行简单的介绍。
在将不同的数据类型序列化时,会使用第一个 byte 来表示当前数据的数据类型,以便在客户端或服务器在处理时能恢复原来的数据格式。
RESP “数据格式”的第一个字节用来表示数据类型,然后逻辑上属于不同部分的内容通过 CRLF(\r\n)分隔。 redisFormatCommandArgv的源码在hiredis.c
/* Format a command according to the Redis protocol. This function takes the
* number of arguments, an array with arguments and an array with their
* lengths. If the latter is set to NULL, strlen will be used to compute the
* argument lengths.
*/
int redisFormatCommandArgv(char **target, int argc, const char **argv, const size_t *argvlen) {
char *cmd = NULL; /* final command */
int pos; /* position in final command */
size_t len;
int totlen, j;
/* Calculate number of bytes needed for the command */
totlen = 1+intlen(argc)+2;
for (j = 0; j < argc; j++) {
len = argvlen ? argvlen[j] : strlen(argv[j]);
totlen += bulklen(len);
}
/* Build the command at protocol level */
cmd = malloc(totlen+1);
if (cmd == NULL)
return -1;
pos = sprintf(cmd,"*%d\r\n",argc);
for (j = 0; j < argc; j++) {
len = argvlen ? argvlen[j] : strlen(argv[j]);
pos += sprintf(cmd+pos,"$%zu\r\n",len);
memcpy(cmd+pos,argv[j],len);
pos += len;
cmd[pos++] = '\r';
cmd[pos++] = '\n';
}
assert(pos == totlen);
cmd[pos] = '\0';
*target = cmd;
return totlen;
}
好了回到主流程,接着客户端进入下一个流程,将outbuf内容写入到套接字描述符上并传输到服务器端。
进入redisGetReply方法,该方法下主要有redisGetReplyFromReader和redisBufferWrite 方法,redisGetReplyFromReader主要用于读取挂起的回复,redisBufferWrite 方法用于将当前outbuf中的内容写入到套接字描述符中,并传输内容。源码如下:
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;
}
此时客户端等待服务器端接收写入。
五、server 端接收写入、执行及返回结果
服务器端依然在进行事件循环,在客户端发来内容的时候触发,对应的文件读取事件,readQueryFromClient。
后续的server的处理参数processInputBuffer,调用processCommand参见https://blog.csdn.net/bohu83/article/details/85009255
server返回结果:sendReplyToClient,将outbuf内容写入到套接字描述符并传输到客户端,主要方法如下:
1 、nwritten = write(fd,c->buf+c->sentlen,c->bufpos-c->sentlen);
2、aeDeleteFileEvent 用于删除 文件写事件
六 、Client收到返回结果
其实获取服务器回复和上文中的发送命令过程基本上差不多,也是redisGetReply获取服务器响应。调用关系如下:
cliReadReply(redis-cli.c)-->redisGetReply(hiredis.c)-->redisGetReply()--> redisBufferRead(hiredis.c)
--> redisGetReplyFromReader(hiredis)-->redisReaderGetReply()
-->cliFormatReplyRaw()
-->fwrite(out,sdslen(out),1,stdout);
static int cliReadReply(int output_raw_strings) {
void *_reply;
redisReply *reply;
sds out = NULL;
int output = 1;
if (redisGetReply(context,&_reply) != REDIS_OK) {
if (config.shutdown) {
redisFree(context);
context = NULL;
return REDIS_OK;
}
if (config.interactive) {
/* Filter cases where we should reconnect */
if (context->err == REDIS_ERR_IO &&
(errno == ECONNRESET || errno == EPIPE))
return REDIS_ERR;
if (context->err == REDIS_ERR_EOF)
return REDIS_ERR;
}
cliPrintContextError();
exit(1);
return REDIS_ERR; /* avoid compiler warning */
}
reply = (redisReply*)_reply;
config.last_cmd_type = reply->type;
/* Check if we need to connect to a different node and reissue the
* request. */
if (config.cluster_mode && reply->type == REDIS_REPLY_ERROR &&
(!strncmp(reply->str,"MOVED",5) || !strcmp(reply->str,"ASK")))
{
char *p = reply->str, *s;
int slot;
output = 0;
/* Comments show the position of the pointer as:
*
* [S] for pointer 's'
* [P] for pointer 'p'
*/
s = strchr(p,' '); /* MOVED[S]3999 127.0.0.1:6381 */
p = strchr(s+1,' '); /* MOVED[S]3999[P]127.0.0.1:6381 */
*p = '\0';
slot = atoi(s+1);
s = strrchr(p+1,':'); /* MOVED 3999[P]127.0.0.1[S]6381 */
*s = '\0';
sdsfree(config.hostip);
config.hostip = sdsnew(p+1);
config.hostport = atoi(s+1);
if (config.interactive)
printf("-> Redirected to slot [%d] located at %s:%d\n",
slot, config.hostip, config.hostport);
config.cluster_reissue_command = 1;
cliRefreshPrompt();
}
if (output) {
if (output_raw_strings) {
out = cliFormatReplyRaw(reply);
} else {
if (config.output == OUTPUT_RAW) {
out = cliFormatReplyRaw(reply);
out = sdscat(out,"\n");
} else if (config.output == OUTPUT_STANDARD) {
out = cliFormatReplyTTY(reply,"");
} else if (config.output == OUTPUT_CSV) {
out = cliFormatReplyCSV(reply);
out = sdscat(out,"\n");
}
}
fwrite(out,sdslen(out),1,stdout);
sdsfree(out);
}
freeReplyObject(reply);
return REDIS_OK;
}
函数主要流程是 调用redisGetReply 获取reply ,结构是redisReply,源码在hiredis.h
/* 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 */
/*字符串值的长度*/
int 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;
- REDIS_REPLY_STRING == 1:返回值是字符串,字符串储存在redis->str当中,字符串长度为redis->len。
- REDIS_REPLY_ARRAY == 2:返回值是数组,数组大小存在redis->elements里面,数组值存储在redis->element[i]里面。数组里面存储的是指向redisReply的指针,数组里面的返回值可以通过redis->element[i]->str来访问,数组的结果里全是type==REDIS_REPLY_STRING的redisReply对象指针。
- REDIS_REPLY_INTEGER == 3:返回值为整数 long long。
- REDIS_REPLY_NIL==4:返回值为空表示执行结果为空。
- REDIS_REPLY_STATUS ==5:返回命令执行的状态,比如set foo bar 返回的状态为OK,存储在str当中 reply->str == "OK"。
- REDIS_REPLY_ERROR ==6 :命令执行错误,错误信息存放在 reply->str当中。
并将数据交给回复解析器处理,也就是cliFormatReplyRaw,该方法将回复内容格式化。最终通过
fwrite(out,sdslen(out),1,stdout);
方法返回给客户端并打印展示给用户。
再看看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
成功发送命令并返回之后,就会开始等待服务端的回复,总共分为两个部分,一是使用 redisBufferRead
从服务端读取原始格式的回复(符合 RESP 协议)该方法主要用于从socket中读取数据。主要方法如下:
nread = read(c->fd,buf,sizeof(buf));
最后的 redisGetReplyFromReader
方法会从 redisContext
中取出 reader
,然后反序列化 RESP 对象,最后打印出来。
限于篇幅,很多函数没有贴出来,关于解析的相关结构体redisReader,也没整理。感兴趣的可以自己看看源码。
总结:
对于redis的常规的命令get\set,理解它执行一条命令执行的整个流程,对于熟悉它整个运行流程较为有利。
当然Linux底层的掌握看起来更有助于理解。
参考:
https://www.jianshu.com/p/0944c16b2353
https://www.jianshu.com/p/9ed98c7cbe6b