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


             Redis服务器是典型的一对多服务器程序,通过使用由IO多路复用技术实现的文件事件处理器,Redis服务器使用单线程单进程的方式来处理命令请求,并与多个客户端进行网络通信。

     

             Redis客户端与服务器之间通过TCP协议进行通信。TCP协议是一种流式协议,数据以字节流的形式进行传递,没有固有的"报文"或"报文边界"的概念,如果需要设置边界,需要应用层自行处理。

             因此,Redis客户端与服务器之间的交互数据,都按照Redis自定义的统一请求协议的格式进行编码。使用这种协议,每条命令之间都有了“边界”。

             举个例子,如果客户端要向服务器发送以下命令请求:

             SET msg “helloworld”

             那么客户端实际发送的数据是:

             *3 $3 SET $3 msg $11 helloworld

             服务器收到这样的数据时,就可以通过解析”*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中,该输入缓存会根据接收到的数据长度动态扩容。接下来对收到的请求数据进行解析,并执行相应的命令处理函数。

             readQueryFromClient函数代码如下:

    void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
        redisClient *c = (redisClient*) privdata;
        int nread, readlen;
        size_t qblen;
        REDIS_NOTUSED(el);
        REDIS_NOTUSED(mask);
    
        server.current_client = c;
        readlen = REDIS_IOBUF_LEN;
        /* If this is a multi bulk request, and we are processing a bulk reply
         * that is large enough, try to maximize the probability that the query
         * buffer contains exactly the SDS string representing the object, even
         * at the risk of requiring more read(2) calls. This way the function
         * processMultiBulkBuffer() can avoid copying buffers to create the
         * Redis Object representing the argument. */
        if (c->reqtype == REDIS_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
            && c->bulklen >= REDIS_MBULK_BIG_ARG)
        {
            int remaining = (unsigned)(c->bulklen+2)-sdslen(c->querybuf);
    
            if (remaining < readlen) readlen = remaining;
        }
    
        qblen = sdslen(c->querybuf);
        if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
        c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
        nread = read(fd, c->querybuf+qblen, readlen);
        if (nread == -1) {
            if (errno == EAGAIN) {
                nread = 0;
            } else {
                redisLog(REDIS_VERBOSE, "Reading from client: %s",strerror(errno));
                freeClient(c);
                return;
            }
        } else if (nread == 0) {
            redisLog(REDIS_VERBOSE, "Client closed connection");
            freeClient(c);
            return;
        }
        if (nread) {
            sdsIncrLen(c->querybuf,nread);
            c->lastinteraction = server.unixtime;
            if (c->flags & REDIS_MASTER) c->reploff += nread;
            server.stat_net_input_bytes += nread;
        } else {
            server.current_client = NULL;
            return;
        }
        if (sdslen(c->querybuf) > server.client_max_querybuf_len) {
            sds ci = catClientInfoString(sdsempty(),c), bytes = sdsempty();
    
            bytes = sdscatrepr(bytes,c->querybuf,64);
            redisLog(REDIS_WARNING,"Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)", ci, bytes);
            sdsfree(ci);
            sdsfree(bytes);
            freeClient(c);
            return;
        }
        processInputBuffer(c);
        server.current_client = NULL;
    }

             该函数中,首先设置每次read读取的最大字节数readlen为REDIS_IOBUF_LEN(16k)。然后得到输入缓存c->querybuf当前长度qblen,也就是已接收到的客户端请求数据的长度。根据qblen更新c->querybuf_peak的值,该属性记录了输入缓存c->querybuf的最大长度。

     

             接下来为c->querybuf扩容,使其能容纳readlen个字节;然后就调用read,最多读取readlen个字节。读取的内容追加到c->querybuf尾部。

             如果read返回值nread为-1,若errno等于EAGAIN,说明暂无数据,置nread为0;否则记录错误信息到日志,释放客户端结构redisClient,并关闭链接,然后直接返回;

             如果read返回0,说明客户端关闭连接,此时记录信息到日志,释放客户端结构redisClient,并关闭链接,然后直接返回;

             read返回非0,说明读取到了数据。判断当前输入缓存c->querybuf的长度是否大于阈值server.client_max_querybuf_len(1G)。若超过阈值,则记录当前客户端信息,以及c->querybuf中前64个字节到日志中,然后释放客户端结构redisClient,并关闭链接,然后直接返回;

     

             最后,调用processInputBuffer解析收到的数据,并在读取到完整的一条命令请求之后,执行相应的命令处理函数。

     

    2:解析处理客户端命令

             Redis服务器收到客户端的请求数据后,调用processInputBuffer函数解析输入缓存redisClient->querybuf中的数据。在得到一条完整的命令请求数据后,就调用processCommand函数处理执行相应的命令。

             processInputBuffer的代码如下:

    void processInputBuffer(redisClient *c) {
        /* Keep processing while there is something in the input buffer */
        while(sdslen(c->querybuf)) {
            /* Return if clients are paused. */
            if (!(c->flags & REDIS_SLAVE) && clientsArePaused()) return;
    
            /* Immediately abort if the client is in the middle of something. */
            if (c->flags & REDIS_BLOCKED) return;
    
            /* REDIS_CLOSE_AFTER_REPLY closes the connection once the reply is
             * written to the client. Make sure to not let the reply grow after
             * this flag has been set (i.e. don't process more commands). */
            if (c->flags & REDIS_CLOSE_AFTER_REPLY) return;
    
            /* Determine request type when unknown. */
            if (!c->reqtype) {
                if (c->querybuf[0] == '*') {
                    c->reqtype = REDIS_REQ_MULTIBULK;
                } else {
                    c->reqtype = REDIS_REQ_INLINE;
                }
            }
    
            if (c->reqtype == REDIS_REQ_INLINE) {
                if (processInlineBuffer(c) != REDIS_OK) break;
            } else if (c->reqtype == REDIS_REQ_MULTIBULK) {
                if (processMultibulkBuffer(c) != REDIS_OK) break;
            } else {
                redisPanic("Unknown request type");
            }
    
            /* Multibulk processing could see a <= 0 length. */
            if (c->argc == 0) {
                resetClient(c);
            } else {
                /* Only reset the client when the command was executed. */
                if (processCommand(c) == REDIS_OK)
                    resetClient(c);
            }
        }
    }

             该函数中,只要c->querybuf不为空,就一直循环处理。在该循环中:

     

             首先,根据客户端的当前状态标志c->flags,判断是否需要继续解析处理,比如:

             如果当前客户端不是SLAVE节点,并且客户端处于阻塞状态,则直接返回;

             如果客户端标志c->flags包含REDIS_BLOCKED,则直接返回;

             如果客户端标志c->flags包含REDIS_CLOSE_AFTER_REPLY,则直接返回。该标志表明发生了异常,服务器不再需要处理客户端请求,在回复客户端错误消息后直接关闭链接。

     

             接下来,如果c->reqtype为0,说明刚要开始处理一条请求(第一次处理c->querybuf中的数据,或刚处理完一条完整的命令请求)。如果数据c->querybuf的首字节为'*',说明该请求会跨越多行(包含多个” ”),则置c->reqtype为EDIS_REQ_MULTIBULK;否则说明该请求为单行请求,置c->reqtype为REDIS_REQ_INLINE;

             如果c->reqtype为REDIS_REQ_INLINE,则调用processInlineBuffer解析单行请求,如果c->reqtype为EDIS_REQ_MULTIBULK,则调用processMultibulkBuffer解析多行请求。这两个函数的返回值如果不是REDIS_OK,则说明尚未收到一条完整的请求,需要退出循环,函数返回后接着读取剩余的数据;

             如果这两个函数返回为REDIS_OK,则说明已经收到并解析好了一条完整的请求,命令的参数已经分解到数组c->argv中,c->argc表示参数个数。

             如果c->argc为0,则无需处理,直接调用resetClient重置客户端状态,也就是释放c->argv数组中的元素,置c->argc、c->reqtype和c->multibulklen为0,置c->bulklen为-1等。然后接着处理c->querybuf中剩下的内容;

             如果c->argc非0,则调用processCommand处理该命令,调用相应的命令处理函数。处理成功后,调用resetClient重置客户端状态。然后接着处理c->querybuf中剩下的内容。

     

             函数processInlineBuffer和processMultibulkBuffer分别解析客户端的单行请求和多行请求。这两个函数返回REDIS_OK,说明已经收到并解析好了一条完整的请求,命令的参数已经分解到数组c->argv中,c->argc表示参数个数。

             如果这俩函数返回REDIS_ERR,要么说明收到的客户端命令请求尚不完整,这其实不是错误,这种情况下函数返回后,服务器需要继续接收客户端请求;要么说明客户端发来的请求不符合统一请求协议的格式要求,这种情况下调用setProtocolError删除c->querybuf相应的内容,并且将客户端的标志位c->flags增加REDIS_CLOSE_AFTER_REPLY标记,从而在回复客户端错误信息后直接关闭连接。

             processMultibulkBuffer函数要比processInlineBuffer稍微复杂一些,直接看一下processMultibulkBuffer的实现:

    int processMultibulkBuffer(redisClient *c) {
        char *newline = NULL;
        int pos = 0, ok;
        long long ll;
    
        if (c->multibulklen == 0) {
            /* The client should have been reset */
            redisAssertWithInfo(c,NULL,c->argc == 0);
    
            /* Multi bulk length cannot be read without a 
     */
            newline = strchr(c->querybuf,'
    ');
            if (newline == NULL) {
                if (sdslen(c->querybuf) > REDIS_INLINE_MAX_SIZE) {
                    addReplyError(c,"Protocol error: too big mbulk count string");
                    setProtocolError(c,0);
                }
                return REDIS_ERR;
            }
    
            /* Buffer should also contain 
     */
            if (newline-(c->querybuf) > ((signed)sdslen(c->querybuf)-2))
                return REDIS_ERR;
    
            /* We know for sure there is a whole line since newline != NULL,
             * so go ahead and find out the multi bulk length. */
            redisAssertWithInfo(c,NULL,c->querybuf[0] == '*');
            ok = string2ll(c->querybuf+1,newline-(c->querybuf+1),&ll);
            if (!ok || ll > 1024*1024) {
                addReplyError(c,"Protocol error: invalid multibulk length");
                setProtocolError(c,pos);
                return REDIS_ERR;
            }
    
            pos = (newline-c->querybuf)+2;
            if (ll <= 0) {
                sdsrange(c->querybuf,pos,-1);
                return REDIS_OK;
            }
    
            c->multibulklen = ll;
    
            /* Setup argv array on client structure */
            if (c->argv) zfree(c->argv);
            c->argv = zmalloc(sizeof(robj*)*c->multibulklen);
        }
    
        redisAssertWithInfo(c,NULL,c->multibulklen > 0);
        while(c->multibulklen) {
            /* Read bulk length if unknown */
            if (c->bulklen == -1) {
                newline = strchr(c->querybuf+pos,'
    ');
                if (newline == NULL) {
                    if (sdslen(c->querybuf) > REDIS_INLINE_MAX_SIZE) {
                        addReplyError(c,
                            "Protocol error: too big bulk count string");
                        setProtocolError(c,0);
                        return REDIS_ERR;
                    }
                    break;
                }
    
                /* Buffer should also contain 
     */
                if (newline-(c->querybuf) > ((signed)sdslen(c->querybuf)-2))
                    break;
    
                if (c->querybuf[pos] != '$') {
                    addReplyErrorFormat(c,
                        "Protocol error: expected '$', got '%c'",
                        c->querybuf[pos]);
                    setProtocolError(c,pos);
                    return REDIS_ERR;
                }
    
                ok = string2ll(c->querybuf+pos+1,newline-(c->querybuf+pos+1),&ll);
                if (!ok || ll < 0 || ll > 512*1024*1024) {
                    addReplyError(c,"Protocol error: invalid bulk length");
                    setProtocolError(c,pos);
                    return REDIS_ERR;
                }
    
                pos += newline-(c->querybuf+pos)+2;
                if (ll >= REDIS_MBULK_BIG_ARG) {
                    size_t qblen;
    
                    /* If we are going to read a large object from network
                     * try to make it likely that it will start at c->querybuf
                     * boundary so that we can optimize object creation
                     * avoiding a large copy of data. */
                    sdsrange(c->querybuf,pos,-1);
                    pos = 0;
                    qblen = sdslen(c->querybuf);
                    /* Hint the sds library about the amount of bytes this string is
                     * going to contain. */
                    if (qblen < (size_t)ll+2)
                        c->querybuf = sdsMakeRoomFor(c->querybuf,ll+2-qblen);
                }
                c->bulklen = ll;
            }
    
            /* Read bulk argument */
            if (sdslen(c->querybuf)-pos < (unsigned)(c->bulklen+2)) {
                /* Not enough data (+2 == trailing 
    ) */
                break;
            } else {
                /* Optimization: if the buffer contains JUST our bulk element
                 * instead of creating a new object by *copying* the sds we
                 * just use the current sds string. */
                if (pos == 0 &&
                    c->bulklen >= REDIS_MBULK_BIG_ARG &&
                    (signed) sdslen(c->querybuf) == c->bulklen+2)
                {
                    c->argv[c->argc++] = createObject(REDIS_STRING,c->querybuf);
                    sdsIncrLen(c->querybuf,-2); /* remove CRLF */
                    c->querybuf = sdsempty();
                    /* Assume that if we saw a fat argument we'll see another one
                     * likely... */
                    c->querybuf = sdsMakeRoomFor(c->querybuf,c->bulklen+2);
                    pos = 0;
                } else {
                    c->argv[c->argc++] =
                        createStringObject(c->querybuf+pos,c->bulklen);
                    pos += c->bulklen+2;
                }
                c->bulklen = -1;
                c->multibulklen--;
            }
        }
    
        /* Trim to pos */
        if (pos) sdsrange(c->querybuf,pos,-1);
    
        /* We're done when c->multibulk == 0 */
        if (c->multibulklen == 0) return REDIS_OK;
    
        /* Still not read to process the command */
        return REDIS_ERR;
    }

             redisClient结构中的multibulklen属性,记录正在解析的一条完整的命令请求中,尚未处理的命令参数的个数。如果c->multibulklen为0,说明当前要解析的是命令请求的开头,格式为"*<n> "。

             这种情况下,首先找到c->querybuf中的第一个' '的位置newline,如果c->querybuf中找不到' ',说明收到的客户端的请求尚不完整,直接返回REDIS_ERR。并且如果c->querybuf目前长度超过64k的话,则反馈给客户端错误信息:"Protocol error: too big mbulk count string",然后调用setProtocolError为客户端标志位c->flags增加REDIS_CLOSE_AFTER_REPLY标记;直接返回REDIS_ERR;

             然后如果(newline-(c->querybuf))大于((signed)sdslen(c->querybuf)-2),说明收到的客户端请求尚不完整(缺少' '),直接返回REDIS_ERR;

     

             接下来就开始解析该行,该行内容的正确格式是"*<n> ",其中<n>是一个表明接下来包含多少个字符串的整数。调用string2ll解析得到其中的整数ll,如果解析失败,或者ll大于1M,则反馈给客户端信息"Protocol error: invalid multibulk length",然后,调用setProtocolError为客户端标志位c->flags增加REDIS_CLOSE_AFTER_REPLY标记,返回REDIS_ERR;

             然后使pos记为c->querybuf下一行首地址的索引;

             如果ll小于等于0,则直接清除c->querybuf中刚刚解析的行,直接返回REDIS_OK;然后将ll赋值到c->multibulklen中。然后根据c->multibulklen的值申请数组c->argv的空间,其数组长度就是c->multibulklen。

     

             得到c->multibulklen的值后,接下来开始依次处理命令请求中的每一个字符串行:

             redisClient结构中的bulklen属性,记录接下来要解析的命令请求行中,包含的字符串的长度。如果c->bulklen为-1,说明当前要解析的,是字符串的长度行,格式为"$<n> "。

             这种情况下,处理过程与c->multibulklen为0时的解析过程类似,不在赘述。解析完后,下一行中包含的字符串长度存储在ll中,ll最大为512M,否则反馈给客户端错误信息:"Protocol error: invalid bulk length",并且调用setProtocolError为客户端标志位c->flags增加REDIS_CLOSE_AFTER_REPLY标记,返回REDIS_ERR;

             然后使pos记为c->querybuf下一行首地址的索引;

             如果字符串长度ll大于等于32k,为了后续创建字符串对象时避免复制大块内存,直接使用c->querybuf创建字符串对象。因此直接将c->querybuf中pos之前的内容删除,置pos为0,并且必要情况下为c->querybuf扩容。最后将ll赋值到c->bulklen中;

     

             接下来开始解析c->querybuf中的字符串行,格式为"xxxx ";

             如果(sdslen(c->querybuf)-pos)小于((unsigned)(c->bulklen+2)),说明收到的客户端请求中,字符串行尚不完整,直接退出循环,返回REDIS_ERR;

             否则,如果同时满足以下三个条件:

    pos == 0;

    c->bulklen >= REDIS_MBULK_BIG_ARG;

    (signed) sdslen(c->querybuf) ==c->bulklen+2);

             说明,当前c->querybuf中,不多不少正好包含的是一个大于32k的大字符串行,这种情况下,为了避免拷贝大块内存,直接使用c->querybuf创建字符串对象,并存储到c->argv中;然后重新创建c->querybuf,并为其扩容为c->bulklen+2,这样可以容纳在后续遇到的大字符串(Assume that if we saw a fat argument we'll see another one likely...);

             如果不满足上面的条件,则创建字符串对象,将c->querybuf+pos的内容复制到该字符串对象中;

     

             处理完一个完整的字符串行后,重置c->bulklen为-1,并且c->multibulklen--;然后循环处理下一个字符串行;

            

             跳出循环后,首先删除已解析的内容,如果c->multibulklen为0,说明已经完整的收到并解析了客户端的一个跨多行的命令请求,返回REDIS_OK,表示可以开始处理该命令了;否则,返回REDIS_ERR,继续接收客户端请求;

     

             processInlineBuffer函数的实现要简单很多,不再赘述。

     

    四:回复客户端

             服务器执行完相应的命令处理函数之后,就会调用addReply类的函数将要回复给客户端的信息写入客户端输出缓存。这些函数包括addReply,addReplySds,addReplyError,addReplyStatus等。

             这些函数首先都会调用prepareClientToWrite函数,注册socket描述符上的可写事件,然后将回复信息写入到客户端输出缓存中。

             redisClient结构中有两种客户端输出缓存,一种是静态大小的数组(buf),一种是动态大小的列表(reply)。追加回复信息时,首先尝试将信息追加到数组buf中,如果其空间不足,则将信息在追加到reply中。比如addReplyString的代码如下:

    void addReplyString(redisClient *c, char *s, size_t len) {
        if (prepareClientToWrite(c) != REDIS_OK) return;
        if (_addReplyToBuffer(c,s,len) != REDIS_OK)
            _addReplyStringToList(c,s,len);
    }

             调用函数_addReplyToBuffer向c->buf中添加数据,如果该函数返回REDIS_ERR,说明添加失败,则调用_addReplyStringToList,将数据添加到c->reply中。其他addReply类的函数也是类似的处理,不再赘述。

     

             每次向客户端输出缓存追加新数据之前,都要调用函数prepareClientToWrite。     因Redis中不同类型的客户端需要不同的处理:有些客户端(比如加载AOF文件时的伪客户端)无需追加新数据,这种情况下,该函数直接返回REDIS_ERR;有些客户端(比如Lua客户端)需要追加新数据,但无需注册socket描述符上的可写事件;有些客户端(普通客户端)需要追加数据,并注册socket描述符上的可写事件;

             因此,调用prepareClientToWrite函数返回REDIS_ERR,则表示无需向输出缓存追加新数据,只有返回REDIS_OK时才需要向输出缓存中追加新数据。

     

             prepareClientToWrite函数的代码如下:

    int prepareClientToWrite(redisClient *c) {
        /* If it's the Lua client we always return ok without installing any
         * handler since there is no socket at all. */
        if (c->flags & REDIS_LUA_CLIENT) return REDIS_OK;
    
        /* Masters don't receive replies, unless REDIS_MASTER_FORCE_REPLY flag
         * is set. */
        if ((c->flags & REDIS_MASTER) &&
            !(c->flags & REDIS_MASTER_FORCE_REPLY)) return REDIS_ERR;
    
        if (c->fd <= 0) return REDIS_ERR; /* Fake client for AOF loading. */
    
        /* Only install the handler if not already installed and, in case of
         * slaves, if the client can actually receive writes. */
        if (c->bufpos == 0 && listLength(c->reply) == 0 &&
            (c->replstate == REDIS_REPL_NONE ||
             (c->replstate == REDIS_REPL_ONLINE && !c->repl_put_online_on_ack)))
        {
            /* Try to install the write handler. */
            if (aeCreateFileEvent(server.el, c->fd, AE_WRITABLE,
                    sendReplyToClient, c) == AE_ERR)
            {
                freeClientAsync(c);
                return REDIS_ERR;
            }
        }
    
        /* Authorize the caller to queue in the output buffer of this client. */
        return REDIS_OK;
    }

             如果当前客户端是Lua客户端,直接返回REDIS_OK,而无需注册socket描述符上的可写事件,因为根本没有socket描述符;

             如果客户端为Master节点,除非设置REDIS_MASTER_FORCE_REPLY标志,否则这种客户端不接收回复,因此直接返回REDIS_ERR;

             如果客户端的socket描述符小于等于0,说明是加载AOF文件时的伪客户端,直接返回REDIS_ERR; 

             如果是普通客户端,或者是在从节点需要接收数据时,如果此前从未注册过socket上的可写事件,则调用aeCreateFileEvent注册socket描述符c->fd上的可写事件,事件回调函数为sendReplyToClient;最后直接返回REDIS_OK;       

     

             当TCP输出缓冲区有一定剩余空间时,socket描述符上的可写事件就会触发,从而调用事件回调函数sendReplyToClient。该函数调用write,将输出缓存中的数据发送出去。函数的代码如下:

    void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {
        redisClient *c = privdata;
        int nwritten = 0, totwritten = 0, objlen;
        size_t objmem;
        robj *o;
        REDIS_NOTUSED(el);
        REDIS_NOTUSED(mask);
    
        while(c->bufpos > 0 || listLength(c->reply)) {
            if (c->bufpos > 0) {
                nwritten = write(fd,c->buf+c->sentlen,c->bufpos-c->sentlen);
                if (nwritten <= 0) break;
                c->sentlen += nwritten;
                totwritten += nwritten;
    
                /* If the buffer was sent, set bufpos to zero to continue with
                 * the remainder of the reply. */
                if (c->sentlen == c->bufpos) {
                    c->bufpos = 0;
                    c->sentlen = 0;
                }
            } else {
                o = listNodeValue(listFirst(c->reply));
                objlen = sdslen(o->ptr);
                objmem = getStringObjectSdsUsedMemory(o);
    
                if (objlen == 0) {
                    listDelNode(c->reply,listFirst(c->reply));
                    c->reply_bytes -= objmem;
                    continue;
                }
    
                nwritten = write(fd, ((char*)o->ptr)+c->sentlen,objlen-c->sentlen);
                if (nwritten <= 0) break;
                c->sentlen += nwritten;
                totwritten += nwritten;
    
                /* If we fully sent the object on head go to the next one */
                if (c->sentlen == objlen) {
                    listDelNode(c->reply,listFirst(c->reply));
                    c->sentlen = 0;
                    c->reply_bytes -= objmem;
                }
            }
            /* Note that we avoid to send more than REDIS_MAX_WRITE_PER_EVENT
             * bytes, in a single threaded server it's a good idea to serve
             * other clients as well, even if a very large request comes from
             * super fast link that is always able to accept data (in real world
             * scenario think about 'KEYS *' against the loopback interface).
             *
             * However if we are over the maxmemory limit we ignore that and
             * just deliver as much data as it is possible to deliver. */
            server.stat_net_output_bytes += totwritten;
            if (totwritten > REDIS_MAX_WRITE_PER_EVENT &&
                (server.maxmemory == 0 ||
                 zmalloc_used_memory() < server.maxmemory)) break;
        }
        if (nwritten == -1) {
            if (errno == EAGAIN) {
                nwritten = 0;
            } else {
                redisLog(REDIS_VERBOSE,
                    "Error writing to client: %s", strerror(errno));
                freeClient(c);
                return;
            }
        }
        if (totwritten > 0) {
            /* For clients representing masters we don't count sending data
             * as an interaction, since we always send REPLCONF ACK commands
             * that take some time to just fill the socket output buffer.
             * We just rely on data / pings received for timeout detection. */
            if (!(c->flags & REDIS_MASTER)) c->lastinteraction = server.unixtime;
        }
        if (c->bufpos == 0 && listLength(c->reply) == 0) {
            c->sentlen = 0;
            aeDeleteFileEvent(server.el,c->fd,AE_WRITABLE);
    
            /* Close connection after entire reply has been sent. */
            if (c->flags & REDIS_CLOSE_AFTER_REPLY) freeClient(c);
        }
    }

             当追加要发送的数据到输出缓存时,首先尝试将其添加到c->buf中;如果c->buf空间不足,则追加到c->reply中。如果使用的是c->buf,则c->bufpos表示其中缓存的数据总量,c->sentlen表示其中已发送的数据量;如果使用的是c->reply,则c->reply_bytes表示列表c->reply中,保存的所有sds字符串占用的内存总字节数,c->sentlen表示列表中的正在发送数据的单块缓存元素中,已发送的数据量。

             函数中的totwritten表示本函数当前已发送的数据量;

     

             在函数中,如果c->bufpos大于0,或者listLength(c->reply)大于0,说明缓存中有数据要发送,进入循环,调用write发送数据,write返回值nwritten小于等于0时,要么是TCP输出缓存无空间,要么是发生了错误,因此直接跳出循环。

             在循环中:如果c->bufpos大于0,说明使用的缓存是c->buf。因此调用write,将c->buf中的剩余数据(c->bufpos- c->sentlen个字节)发送出去。如果write返回值nwritten小于等于0时,直接跳出循环;否则,将nwritten增加到c->sentlen和totwritten中,继续下一轮循环写入。如果c->buf中的数据已全部发送出去,则重置c->bufpos和c->sentlen为0,表示清空缓存c->buf;

     

             否则的话,表示使用的缓存是列表c->reply。得到其头结点中保存的字符串对象o,然后得到该字符串的长度objlen,以及该字符串占用的内存objmem。接着调用write,将o->ptr中未发送的数据(objlen - c->sentlen个字节)全部发送出去。如果write返回值nwritten小于等于0时,直接跳出循环;否则,将nwritten增加到c->sentlen和totwritten中,继续下一轮循环写入。如果c->sentlen等于objlen,说明当前节点的数据已经全部发送完成,直接删除该节点,并重置c->sentlen为0,并从c->reply_bytes中减去objmem;

             接下来,将本次已发送的字节数totwritten加到server.stat_net_output_bytes中。

     

             因本函数是可写事件的回调函数,为了避免该函数执行时间过长,而影响其他事件的处理。因此这里限制该函数最大发送的字节数为REDIS_MAX_WRITE_PER_EVENT(64k),一旦已发送的字节数totwritten超过了该值,并且在没设置最大内存限制,或者尚未超过设置的最大内存限制的条件下,直接退出循环,停止发送。

     

             退出循环后,如果write出错,并且errno为EAGAIN,说明TCP输出缓存无空间了,这种情况不是错误,直接置nwritten = 0即可;否则需要记录错误日志,并且调用freeClient释放redisClient,关闭与客户端的连接;

             最后,如果缓存中所有的数据都已经发送完成,则置c->sentlen为0,并且删除socket描述符c->fd上的可写事件;如果客户端标志c->flags中设置了REDIS_CLOSE_AFTER_REPLY,则调用freeClient释放redisClient,关闭与客户端的连接。

     

             其他相关代码,可以参考:

    https://github.com/gqtc/redis-3.0.5/blob/master/redis-3.0.5/src/networking.c

  • 相关阅读:
    5、依赖倒转原则
    4、开放-封闭原则
    3、单一职责原则
    2013年工作生活总结
    2、策略模式
    1、简单工厂模式
    Unity3D笔记四 基础知识概念
    PythonStudy——PyCharm使用技巧 Column Selection Mode(列选择模式)
    PythonStudy——函数对象的案例
    PythonStudy——函数嵌套定义 Function nesting definition
  • 原文地址:https://www.cnblogs.com/gqtcgq/p/7247057.html
Copyright © 2020-2023  润新知