Redis源码解析:14Redis服务器与客户端间的交互

本文深入解析Redis服务器如何处理客户端的命令请求。Redis采用单线程模型,通过IO多路复用技术处理多个客户端连接。客户端与服务器间通信遵循自定义的请求协议,如SET msg "helloworld"会被编码成特定格式。服务器接收到数据后,通过解析边界获取完整命令,执行并回复客户端。Redis服务器初始化时创建事件循环,监听多个端口,对每个连接创建redisClient结构体,管理客户端状态。当接收到客户端请求时,通过readQueryFromClient读取数据,processInputBuffer解析命令,processCommand执行命令,最后通过addReply类函数回复客户端。
摘要由CSDN通过智能技术生成

         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中,该输入缓存会根据接收到的数据长度动态扩容。接下来对收到的请求数据进行解析,并执行相应的命令处

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值