前面我们了解过了当Redis执行一个命令时,服务端做了哪些事情,不了解的同学可以看一下这篇文章走近源码:Redis如何执行命令。今天就一起来看看Redis的命令执行过程中客户端都做了什么事情。
启动客户端
首先看redis-cli.c文件的main函数,也就是我们输入redis-cli命令时所要执行的函数。main函数主要是给config变量的各个属性设置默认值。比如:
- hostip:要连接的服务端的IP,默认为127.0.0.1
- hostport:要连接的服务端的端口,默认为6379
- interactive:是否是交互模式,默认为0(非交互模式)
- 一些模式的设置,例如:cluster_mode、slave_mode、getrdb_mode、scan_mode等
- cluster相关的参数
……
接着调用parseOptions()函数来处理参数,例如-p、-c、–verbose等一些用来指定config属性的(可以输入redis-cli –help查看)或是指定启动模式的。
处理完这些参数后,需要把它们从参数列表中去除,剩下用于在非交互模式中执行的命令。
parseEnv()用来判断是否需要验证权限,紧接着就是根据刚才的参数判断需要进入哪种模式,是cluster还是slave又或者是RDB……如果没有进入这些模式,并且没有需要执行的命令,那么就进入交互模式,否则会进入非交互模式。
1 | /* Start interactive mode when no command is provided */ |
连接服务器
cliConnect()函数用于连接服务器,它的参数是一个标志位,如果是CC_FORCE(0)表示强制重连,如果是CC_QUIET(2)表示不打印错误日志。
如果建立了socket,那么就连接这个socket,否则就去连接指定的IP和端口。
1 | if (config.hostsocket == NULL) { |
redisConnect
redisConnect()(在deps/hiredis/hiredis.c文件中)函数用于连接指定的IP和端口的redis实例。它的返回值是redisContext类型的。这个结构封装了一些客户端与服务端之间的连接状态,obuf是用来存放返回结果的缓冲区,同时还有客户端与服务端的协议。
1 | //hiredis.h |
redisConnect的实现比较简单,首先初始化一个redisContext变量,然后把客户端的flags字段设置为阻塞状态,接着调用redisContextConnectTcp命令。
1 | redisContext *redisConnect(const char *ip, int port) { |
redisContextConnectTcp
redisContextConnectTcp()函数在net.c文件中,它调用的是_redisContextConnectTcp()这个函数,所以我们主要关注这个函数。它用来与服务端创建TCP连接,首先调整了tcp的host和timeout字段,然后getaddrinfo获取要连接的服务信息,这里兼容了IPv6和IPv4。然后尝试连接服务端。
1 | if (connect(s,p->ai_addr,p->ai_addrlen) == -1) { |
connect()函数用于去连接服务器,连接上之后,服务器端会调用accept函数。如果连接失败,也会根据情况决定是否要关闭redisContext文件描述符。
发送命令并接收返回
当客户端和服务端建立连接之后,客户端向服务器端发送命令并接收返回值了。
repl
我们回到redis-cli.c文件中的repl()函数,这个函数就是用来向服务器端发送命令并且接收到的结果返回。
这里首先调用了cliInitHelp()和cliIntegrateHelp()这两个函数,初始化了一些帮助信息,然后设置了一些回调的方法。如果是终端模式,则会从rc文件中加载历史命令。然后调用linenoise()函数读取用户输入的命令,并以空格分隔参数。
1 | nread = read(l.ifd,&c,1); |
接下来是判断是否需要过滤掉重复的参数。
issueCommandRepeat
生成好命令后,就调用issueCommandRepeat()函数开始执行命令。
1 | static int issueCommandRepeat(int argc, char **argv, long repeat) { |
这个函数会调用cliSendCommand()函数,将命令发送给服务器端,如果发送失败,会强制重连一次,然后再次发送命令。
redisAppendCommandArgv
cliSendCommand()函数又会调用redisAppendCommandArgv()函数(在hiredis.c文件中)这个函数是按照Redis协议将命令进行编码。
cliReadReply
然后调用cliReadReply()函数,接收服务器端返回的结果,调用cliFormatReplyRaw()函数将结果进行编码并返回。
举个栗子
我们以GET命令为例,具体描述一下,从客户端到服务端,程序是如何运行的。
我们用gdb调试redis-server,将断点设置到readQueryFromClient函数这里。
1 | gdb src/redis-server |
然后再调试redis-cli,断点设置cliReadReply函数。
1 | gdb src/redis-cli |
在客户端输入get命令,发现程序在断点处停止。
1 | 127.0.0.1:6379> get jackey |
我们可以看到这时Redis已经准备好将命令发送给服务端了,先来查看一下要发送的内容。
1 | (gdb) p context->obuf |
把\r\n替换成换行符看的后是这样:
1 | *2 |
*2表示命令参数的总数,包括命令的名字,也就是告诉服务端应该处理两个参数。
$3表示第一个参数的长度。
get是命令名,也就是第一个参数。
$6表示第二个参数的长度。
jackey是第二个参数。
当程序运行到redisGetReply时就会把命令发送给服务端了,这时我们再来看服务端的运行情况。
1 | Thread 1 "redis-server" hit Breakpoint 1, readQueryFromClient ( |
程序调整到
1 | sdsIncrLen(c->querybuf,nread); |
这时nread的内容会被加到c->querybuf中,我们来看一下是不是我们发送过来的命令。
1 | (gdb) p c->querybuf |
到这里,Redis的服务端已经接受到请求了。接下来就是处理命令的过程,前文我们提到Redis是在processCommand()函数中处理的。
processCommand()函数会调用lookupCommand()函数,从redisCommandTable表中查询出要执行的函数。然后调用c->cmd->proc(c)执行这个函数,这里我们get命令对应的是getCommand函数,getCommand里只是调用了getGenericCommand()函数。
1 | //t_string.c |
lookupKeyReadOrReply()用来查找指定key存储的内容。并返回一个Redis对象,它的实现在db.c文件中。
1 | robj *lookupKeyReadOrReply(client *c, robj *key, robj *reply) { |
在lookupKeyReadWithFlags函数中,会先判断这个key是否过期,如果没有过期,则会继续调用lookupKey()函数进行查找。
1 | robj *lookupKey(redisDb *db, robj *key, int flags) { |
在这个函数中,先调用了dictFind函数,找到key对应的entry,然后再从entry中取出val。
找到val后,我们回到getGenericCommand函数中,它会调用addReplyBulk函数,将返回值添加到client结构的buf字段。
1 | (gdb) p c->buf |
到这里,get命令的处理过程已经完结了,剩下的事情就是将结果返回给客户端,并且等待下次命令。
客户端收到返回值后,如果是控制台输出,则会调用cliFormatReplyTTY对结果进行解析
1 | (gdb) n |
最后将结果输出。