Redis事件驱动(2)- 注册

在创建完文件描述符FD之后,Redis就拥有了一个可以监听客户端事件的句柄了。

注意在创建fd之后,实际上并没有开始接受客户端连接。

接受客户端连接请求过程分两个部分,

· accept

· wait

accept做的事情是等待客户端连接。
当我们在服务器上运行了 redis-server,Redis会启动并开始初始化,在初始化完成时,Redis持有了一个文件描述符,并且开始等待连接。

直到有一个客户端启动了 redis-cli,
这个时候服务端才会收到一个连接请求。

下面分accept和wait两个部分说明注册的逻辑。

accept

accept可以细分为两个步骤,

  1. 打开6379默认端口,创建socket连接监听描述符。
  2. 创建socket连接的文件描述符。
创建监听描述符

Redis允许通过ipv4和ipv6两种方式来连接服务端。

在 aeCreateEventLoop 完成后,跟着就是打开端口开始监听,

redis.c line 1679

/* Open the TCP listening socket for the user commands. */
    if (server.port != 0 &&
        listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR)

ListenToPort 做了一件事,它打开了默认端口,然后设置socket的协议族等参数,创建一个文件描述符s,

从listenToPort往下跟,会来到 anet.c,按划分这个文件属于网络模块。
最终来到 _anetTcpServer() 函数。

static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backlog)
{
    int s, rv;
    char _port[6];  /* strlen("65535") */
    struct addrinfo hints, *servinfo, *p;

    snprintf(_port,6,"%d",port);
    memset(&hints,0,sizeof(hints));
    hints.ai_family = af;
    hints.ai_socktype = SOCK_STREAM;
    hints.ai_flags = AI_PASSIVE;    /* No effect if bindaddr != NULL */

    if ((rv = getaddrinfo(bindaddr,_port,&hints,&servinfo)) != 0) {
        anetSetError(err, "%s", gai_strerror(rv));
        return ANET_ERR;
    }
    for (p = servinfo; p != NULL; p = p->ai_next) {
        if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1) // <- 创建socket
            continue;

        if (af == AF_INET6 && anetV6Only(err,s) == ANET_ERR) goto error;
        if (anetSetReuseAddr(err,s) == ANET_ERR) goto error;
        if (anetListen(err,s,p->ai_addr,p->ai_addrlen,backlog) == ANET_ERR) goto error;
        goto end;
    }
    if (p == NULL) {
        anetSetError(err, "unable to bind socket");
        goto error;
    }

error:
    s = ANET_ERR;
end:
    freeaddrinfo(servinfo);
    return s;
}

上面代码注释的部分,就是创建一个socket连接了。返回的是一个文件描述符s,这个s会给到哪里呢。这要来看 listToPort的源码,

redis.c line 1573

int listenToPort(int port, int *fds, int *count) {
    int j;

    /* Force binding of 0.0.0.0 if no bind address is specified, always
     * entering the loop if j == 0. */
    if (server.bindaddr_count == 0) server.bindaddr[0] = NULL;
    for (j = 0; j < server.bindaddr_count || j == 0; j++) {
        if (server.bindaddr[j] == NULL) {
            /* Bind * for both IPv6 and IPv4, we enter here only if
             * server.bindaddr_count == 0. */
            fds[*count] = anetTcp6Server(server.neterr,port,NULL,
                server.tcp_backlog); // 创建 socket 的调用
            if (fds[*count] != ANET_ERR) {
                anetNonBlock(NULL,fds[*count]);
                (*count)++;
            }
            fds[*count] = anetTcpServer(server.neterr,port,NULL,
                server.tcp_backlog); // 创建 socket 的调用
            if (fds[*count] != ANET_ERR) {
                anetNonBlock(NULL,fds[*count]);
                (*count)++;
            }
            /* Exit the loop if we were able to bind * on IPv4 or IPv6,
             * otherwise fds[*count] will be ANET_ERR and we'll print an
             * error and return to the caller with an error. */
            if (*count) break;
        } else if (strchr(server.bindaddr[j],':')) {
            /* Bind IPv6 address. */
            fds[*count] = anetTcp6Server(server.neterr,port,server.bindaddr[j],
                server.tcp_backlog);
        } else {
            /* Bind IPv4 address. */
            fds[*count] = anetTcpServer(server.neterr,port,server.bindaddr[j],
                server.tcp_backlog);
        }
        if (fds[*count] == ANET_ERR) {
            redisLog(REDIS_WARNING,
                "Creating Server TCP listening socket %s:%d: %s",
                server.bindaddr[j] ? server.bindaddr[j] : "*",
                server.port, server.neterr);
            return REDIS_ERR;
        }
        anetNonBlock(NULL,fds[*count]);
        (*count)++;
    }
    return REDIS_OK;
}

上面代码注释的部分就是调用 _anetTcpServer() 的地方。

注意看 fds[],它是从参数来的,redis.c 的调用中传进来 listenToPort 的第二个参数是个指针,

也就是这里的 fds。

当我们创建完socket之后会得到一个文件描述符s,在listenToPort 这里就会把s赋值给 fds。

回到redis.c中,你会发现刚刚调用 listenToPort 那里,第二个参数就是 server.ipfd 了

if (server.port != 0 &&
        listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR)

现在记住 server.ipfd 保存的是和 socket 相关的fd。

创建连接描述符

在 redis.c 的1747行。

void initServer(void) {
    ...
    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.");
            }
    }

这部分代码在创建event loop之后,意味着在accept客户端连接之前,必须要有一个可用的EL(event loop),在这之前的客户端连接是无效的。

主要看这个函数,

aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL)

首先,server.ipfd[] ,眼熟不。

上面说到在端口6379上打开了socket,得到一个文件描述符s,然后赋值给 server.ipfd。

这里redis把他作为参数用来创建一个文件事件。

然后是 acceptTcpHandler,这是个函数指针,它的实现在 networking.c 的 570 行。

这个函数分两部分分析,

· aeCreateFileEvent()

· acceptTcpHandler()

aeCreateFileEvent是真正创建监听描述符的函数。而acceptTcpHandler是处理当监听到客户端连接了,后续要做的操作。

aeCreateFileEvent

这是整个Redis里创建文件事件的主函数,到处都会看到它,甚至会看到有些函数反复调用。

但不要慌!它不过是个函数。

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    aeFileEvent *fe = &eventLoop->events[fd];

    if (aeApiAddEvent(eventLoop, fd, mask) == -1) //根据 fd 创建一个监听fd的文件描述符
        return AE_ERR;
    fe->mask |= mask;
    if (mask & AE_READABLE) fe->rfileProc = proc;
    if (mask & AE_WRITABLE) fe->wfileProc = proc;
    fe->clientData = clientData;
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd;
    return AE_OK;
}

函数实现非常简洁。重点关注中间调用 aeApiAddEvent 的地方。

前面说过Redis会根据不同的系统架构使用不同的函数库,aeApiAddEvent 在Redis中也有四个不同的实现。

还是默认以 ae_epoll 中的实现为例来分析。

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata; //取el中的 apidata 
    struct epoll_event ee;
    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. */
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;

    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.u64 = 0; /* avoid valgrind warning */
    ee.data.fd = fd;
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1; //注册文件描述符
    return 0;
}

首先它从Redis中的eventloop 里获取 apidata ,这是个指针,意味着它想直接改变它的值。

在中间经过一些参数初始化后,就调用系统内核 epoll_ctl 函数注册文件描述符。

传进去的 epfd,注意是从 eventloop 里获取的,它是全局唯一的。

也就意味着Redis其实只创建了一个文件描述符 epfd,用于监听所有的其他的文件描述符。

这是个很关键的地方,因为后面的eventloop是个while(true)循环,在循环里不断地从 epfd 里取数据。如果没有弄明白这个 epfd -> fd[] 的一对多映射关系,就会弄不清楚Redis的核心逻辑。

在 epoll_ctl 执行完之后,如果结果不是-1的话就说明成功了。

然后Redis会得到一个全局唯一的 epfd 文件描述符。

回到 aeCreateFileEvent 中,

int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    aeFileEvent *fe = &eventLoop->events[fd]; //以fd为下标取一个文件事件结构体

    if (aeApiAddEvent(eventLoop, fd, mask) == -1)
        return AE_ERR;
    fe->mask |= mask; 
    if (mask & AE_READABLE) fe->rfileProc = proc; // 把 proc 指针函数赋值给这个文件事件
    if (mask & AE_WRITABLE) fe->wfileProc = proc; // 把 proc 指针函数赋值给这个文件事件
    fe->clientData = clientData;
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd;
    return AE_OK;
}

&eventLoop->events[fd] 在redis初始化的时候就已经创建好并分配内存了,这时候只是从数组里按文件描述符取对应的可用event结构体出来。

在注册文件描述符成功之后,就可以把函数指针赋值给这个文件事件了。

这里有个疑问,Redis把文件事件放进去了,

那什么时候取,怎么取?

简单说的话,Redis会启动一个循环,不断地读取 epfd ,看看是不是有新数据进来。

内核在fd发生数据IO的时候会有触发 epfd 的电平变化,如果发现 epfd 有数据。那么只要从 events[] 中按对应的 fd下标取 aeFileEvent 结构体,就可以拿到对应的处理函数 proc了。

这个循环就是事件驱动了,也叫事件循环,在 ae.c 的 aeMain() 中可以看到。

acceptTcpHandler

说完文件描述符注册,接下来是 acceptTcpHandler 是什么逻辑了。

前面说到在 accept之前,Redis只是创建了个链接,但并没有真正和客户端连接上。需要客户端发起请求,服务端accept了才真正开始。

Redis在socket上收到客户端连接请求之后,fd会触发高电平。

linux内核在这个时候会把这个事情通知给Redis注册的 epfd,eventloop在发现epfd有数据进来的时候就会取到这个 aeFileEvent结构体,而acceptTcpHandler就保存在这个结构体中。

acceptTcpHandler的真正实现是在 networking.c 的542行。

static void acceptCommonHandler(int fd, int flags) {
    redisClient *c;
    if ((c = createClient(fd)) == NULL) { // 对每个客户端创建一个 redisClient
        redisLog(REDIS_WARNING,
            "Error registering fd event for the new client: %s (fd=%d)",
            strerror(errno),fd);
        close(fd); /* May be already closed, just ignore errors */
        return;
    }
    /* If maxclient directive is set and this is one client more... close the
     * connection. Note that we create the client instead to check before
     * for this condition, since now the socket is already set in non-blocking
     * mode and we can send an error for free using the Kernel I/O */
    if (listLength(server.clients) > server.maxclients) { //超出连接数量,返回错误信息
        char *err = "-ERR max number of clients reached\r\n";

        /* That's a best effort error message, don't check write errors */
        if (write(c->fd,err,strlen(err)) == -1) {
            /* Nothing to do, Just to avoid the warning... */
        }
        server.stat_rejected_conn++;
        freeClient(c);
        return;
    }
    server.stat_numconnections++;
    c->flags |= flags;
}

到这里就真正的完成了接收连接的工作。

代码逻辑比较简单,这里有一个比较精妙的设计是在超出连接后该怎么办?

作者对这种情况的设计是,

1.首先允许建立连接
2.通过这个连接返回错误信息
3.如果返回错误信息失败也不做过多的尝试,反正已经尽力了
4.关闭这个连接

你会发现其实这个连接是可以建立的,并不是因为性能等各项原因不能建立。而且作者也不会对它做过多的错误尝试,反正也不关心客户端能不能收到错误信息。

这就是避免过度设计的哲学体现。

wait

wait才是Redis的重头戏。

上面说Redis创建了一个全局的文件描述符 epfd,通过eventloop去判断它是否有数据进来。

这部分的代码逻辑在 ae.c 的 352 行

/* Process every pending time event, then every pending file event
 * (that may be registered by time event callbacks just processed).
 * Without special flags the function sleeps until some file event
 * fires, or when the next time event occurs (if any).
 *
 * If flags is 0, the function does nothing and returns.
 * if flags has AE_ALL_EVENTS set, all the kind of events are processed.
 * if flags has AE_FILE_EVENTS set, file events are processed.
 * if flags has AE_TIME_EVENTS set, time events are processed.
 * if flags has AE_DONT_WAIT set the function returns ASAP until all
 * the events that's possible to process without to wait are processed.
 *
 * The function returns the number of events processed. */
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    ...
    numevents = aeApiPoll(eventLoop, tvp); //line 400

上面省略了一些代码,重头戏在 400行这里。

aeApiPoll,照惯例分析 ae_epoll 的 aeApiPoll 函数。

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
            tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1); //真正的读取时间的地方
    if (retval > 0) {
        int j;

        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events+j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}

在这个函数里面会调用内核接口 epoll_wait,

在指定的时间参数 tvp内,如果没有事件发生则直接返回。

有事件发生,retval则是总的事件数量。

比如有一个客户端连接,客户端发送了SET命令,那么retval就是1,两个客户端这里就是2。

可以在这里打印系统日志跟踪实际的执行情况。

回到 ae.c 中,

        numevents = aeApiPoll(eventLoop, tvp);
        for (j = 0; j < numevents; j++) {
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int rfired = 0;

	    /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn't
             * processed, so we check if the event is still valid. */
            if (fe->mask & mask & AE_READABLE) {
                rfired = 1;
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
            }
            if (fe->mask & mask & AE_WRITABLE) {
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
            }
            processed++;
        }

在 aeApiPoll接下来的代码里Redis做了一件事,

根据发生的事件描述符,取对应的 aeFileEvent 结构体,还记得这些结构体的proc指针吗,

proc指针保存了一开始 aeCreateFileEvent 的时候传进去的函数指针。

这里就是真正地执行proc函数指针的地方。

小结

Redis的事件驱动实际还是比较复杂的,需要花不少时间反复看代码才能清晰地理解里面的操作逻辑。

里面有非常多精妙的设计,看完事件驱动之后能明白为什么Redis是人类编程精华。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值