Redis服务器是典型的一对多服务器程序,通过使用由IO多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。
Redis客户端与服务器之间通过TCP协议进行通信。TCP协议是一种流式协议,数据以字节流的形式进行传递,没有固有的"报文"或"报文边界"的概念,如果需要设置边界,需要应用层自行处理。
因此,Redis客户端与服务器之间的交互数据,都按照Redis自定义的统一请求协议的格式进行编码。使用这种协议,每条命令之间都有了“边界”。
举个例子,如果客户端要向服务器发送以下命令请求:
SET msg “helloworld”
那么客户端实际发送的数据是:
*3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$11\r\nhelloworld\r\n
服务器收到这样的数据时,就可以通过解析”*3”得到该命令有3个参数,第一个参数长度为3,值为”SET”,也就是要执行的命令;第二个参数长度为3,值为”msg”;第三个参数长度为11,值为”hello world”。
这样就得到了一条完整的命令,解析并处理该命令后,接着解析下一条命令。
一:客户端结构redisClient
对于每个与服务器进行连接的客户端,服务器都为这些客户端建立了相应的redisClient结构,该结构体定义在redis.h中,它的定义如下(有省略):
typedef struct redisClient {
uint64_t id; /* Client incremental unique ID. */
int fd;
redisDb *db;
int dictid;
robj *name; /* As set by CLIENT SETNAME */
sds querybuf;
size_t querybuf_peak; /* Recent (100ms or more) peak of querybuf size */
int argc;
robj **argv;
struct redisCommand *cmd, *lastcmd;
int reqtype;
int multibulklen; /* number of multi bulk arguments left to read */
long bulklen; /* length of bulk argument in multi bulk request */
list *reply;
unsigned long reply_bytes; /* Tot bytes of objects in reply list */
...
int flags; /* REDIS_SLAVE | REDIS_MONITOR | REDIS_MULTI ... */
int authenticated; /* when requirepass is non-NULL */
...
/* Response buffer */
int bufpos;
char buf[REDIS_REPLY_CHUNK_BYTES];
} redisClient;
这个结构保存了客户端当前的状态信息,以及执行相关功能时需要用到的数据结构,比如:客户端的socket描述符(fd),指向客户端正在使用的数据库的指针(db),客户端的名字(name),客户端的标志值(flags),客户端输入缓存(querybuf),客户端当前要执行的命令参数(argv),以及参数个数(argc),以及客户端的输出缓存(buf和reply)等。
这些属性的具体意义会在下面的章节中介绍。
二:初始化(创建监听端口、注册建连事件)
在Redis服务器的初始化函数initserver中,调用aeCreateEventLoop创建了Redis服务器中唯一的事件循环结构(aeEventLoop):server.e1。server.e1是全局性的,Redis服务器中所有的事件都注册在该结构上。
默认情况下,Redis服务器监听本地所有网络接口上的连接(0.0.0.0)。可以在配置文件中,通过"bind"选项设置监听的地址,其后跟一个或多个空格分隔的IP地址,比如:
bind 192.168.1.100 10.0.0.1
Redis将这些地址保存在server.bindaddr中,IP地址总数为server.bindaddr_count。
在initserver函数中,调用listenToPort,根据这些监听地址,调用socket、bind和listen创建监听socket描述符。
/* Open the TCP listening socket for the user commands. */
if (server.port != 0 &&
listenToPort(server.port, server.ipfd, &server.ipfd_count) == REDIS_ERR)
exit(1);
创建好的监听描述符保存在描述符数组server.ipfd中,最后创建的监听描述符的总数为server.ipfd_count。server.ipfd数组为固定大小:REDIS_BINDADDR_MAX(16),因此最多只支持16个监听地址。
然后,针对每个监听描述符,调用aeCreateFileEvent,注册其上的可读事件,回调函数为acceptTcpHandler:
for (j = 0; j < server.ipfd_count; j++) {
if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
acceptTcpHandler,NULL) == AE_ERR)
{
redisPanic(
"Unrecoverable error creating server.ipfd file event.");
}
}
Redis服务器收到客户端的TCP连接后,就会调用acceptTcpHandler函数进行处理。acceptTcpHandler函数的代码如下:
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
char cip[REDIS_IP_STR_LEN];
REDIS_NOTUSED(el);
REDIS_NOTUSED(mask);
REDIS_NOTUSED(privdata);
while(max--) {
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) {
if (errno != EWOULDBLOCK)
redisLog(REDIS_WARNING,
"Accepting client connection: %s", server.neterr);
return;
}
redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
acceptCommonHandler(cfd,0);
}
}
该函数每次最多处理MAX_ACCEPTS_PER_CALL(1000)个连接,如果还有其他连接,则等到下次调用acceptTcpHandler时再处理,这样做的原因是为了保证该函数的执行时间不会过长,以免影响后续事件的处理。
针对每个连接,调用anetTcpAccept函数进行accept,并将客户端地址记录到cip以及cport中;
建链后的socket描述符为cfd,根据该值调用acceptCommonHandler,该函数中,调用createClient创建一个redisClient结构,并注册socket描述符上的可读事件,回调函数为readQueryFromClient。最后将该redisClient结构存储到全局客户端列表server.clients中;
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}
三:接收客户端请求,解析并处理请求
1:接收数据
Redis服务器收到客户端的请求数据后,就会触发socket描述符上的可读事件,从而调用其回调函数readQueryFromClient。
在readQueryFromClient中,调用read读取客户端的请求,并缓存到redisClient结构中的输入缓存querybuf中,该输入缓存会根据接收到的数据长度动态扩容。接下来对收到的请求数据进行解析,并执行相应的命令处