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

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

         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的首字节为'*',说明该请求会跨越多行(包含多个”\r\n”),则置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 \r\n */
        newline = strchr(c->querybuf,'\r');
        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 \n */
        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,'\r');
            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 \n */
            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 \r\n) */
            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>\r\n"。

         这种情况下,首先找到c->querybuf中的第一个'\r'的位置newline,如果c->querybuf中找不到'\r',说明收到的客户端的请求尚不完整,直接返回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),说明收到的客户端请求尚不完整(缺少'\n'),直接返回REDIS_ERR;

 

         接下来就开始解析该行,该行内容的正确格式是"*<n>\r\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>\r\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\r\n";

         如果(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

posted @ 2016-04-09 17:07  gqtc  阅读(2274)  评论(0编辑  收藏  举报