redis4.0的文件事件与客户端

简介

文件事件的流程大概如下:

  1. 在服务器初始化时生成aeEventLoop并赋值给server,接着创建监听TCP连接事件。
  2. 处理TCP连接时会创建client类型的对象,将其绑定在accept函数返回的文件描述符fd上,并对fd注册一个可读事件,当客户端数据来临时,readQueryFromClient会对数据进行处理。
  3. redis处理完数据后,会调用write函数将数据返回给客户端(但不是在一个循环里)。如果函数返回的值小于写入的值,说明系统缓存区空间不够,或者文件描述符在中途被占用,那么redis会注册一个可写事件,当可写事件触发时,sendReplyToClient函数会写入剩余的数据。
  4. 当客户端断开连接,服务器会释放client相关的资源,随之删除对应的文件事件。

正文

准备阶段

在初始化服务器时,server函数创建clientsclients_pending_write等字段,并通过aeCreateEventLoop创建一个aeEventLoop对象。

aeEventLoop *aeCreateEventLoop(int setsize) {
aeEventLoop *eventLoop;
int i; eventLoop = zmalloc(sizeof(*eventLoop));
eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
eventLoop->setsize = setsize;
if (aeApiCreate(eventLoop) == -1) goto err;
/* Events with mask == AE_NONE are not set.*/
for (i = 0; i < setsize; i++)
eventLoop->events[i].mask = AE_NONE;
return eventLoop;
}

此处传入参数setsize的值为maxClients+128,maxClients默认值为10,000。events用于存放注册的文件事件,而fired则在事件触发时,存放被触发的事件。两者的长度都为setsize大小。

紧接着便会注册第一个文件事件。

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

这里我们更进一步看下aeCreateFileEvent的代码, fd文件描述符 被用于偏移来获取对应的文件事件结构,因此fd的值必须小于之前注册的事件大小的值。第一个被用于注册文件事件的fd用于监听TCP连接,由于进程启动时会打开一些其他的文件,因此eventLoop->events的空间并没有并完全利用。此处还通过mask来注册对应的事件触发后的处理函数。如果是监听可读事件,那么rfileProc处理函数会被赋值。可写事件同理。此时并没有传入clientData,我们会在下文再回到这个函数。

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)
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;
}

接受客户端连接

当接受来自客户端连接时,便会调用acceptTcpHandler函数,该函数会接受所有客户端的请求,但一次最多接受MAX_ACCEPTS_PER_CALL1000个客户端,并且如果在轮询中发现没有客户端请求,就会立刻返回。接受了一个客户端连接请求后,便会进入处理函数acceptCommonHandler,它会创建一个client的对象,如果连接的数量大于设置的值,则会断开连接。如果redis跑在保护模式,则可能返回错误信息。

If no pending connections are present on the queue, and the socket is
not marked as nonblocking, accept() blocks the caller until a
connection is present. If the socket is marked nonblocking and no
pending connections are present on the queue, accept() fails with the
error EAGAIN or EWOULDBLOCK.

最主要的代码位于createClient,它会注册客户端可读事件,关联readQueryFromClient函数,并且初始化client的一些属性。

client *createClient(int fd) {
client *c = zmalloc(sizeof(client)); if (fd != -1) {
anetNonBlock(NULL,fd);
anetEnableTcpNoDelay(NULL,fd);
if (server.tcpkeepalive)
anetKeepAlive(NULL,fd,server.tcpkeepalive);
if (aeCreateFileEvent(server.el,fd,AE_READABLE,
readQueryFromClient, c) == AE_ERR)
{
close(fd);
zfree(c);
return NULL;
}
} selectDb(c,0);
uint64_t client_id; client_id = server.next_client_id;
server.next_client_id += 1; c->id = client_id;
c->fd = fd;
c->name = NULL;
c->bufpos = 0; //下一个返回数据存入位置
// c->buf 数组存储返回给客户端的数据
c->querybuf = sdsempty(); //查询缓存
c->reqtype = 0; //查询类型 一般为multi
c->argc = 0; //参数个数 由querybuf解析而得
c->argv = NULL;//参数值 由querybuf解析而得
c->cmd = c->lastcmd = NULL;
c->multibulklen = 0; //查询数据的行数
c->bulklen = -1;//一行查询数据的长度
c->sentlen = 0;//已经发送的数据长度
c->flags = 0;
c->ctime = c->lastinteraction = server.unixtime;
c->reply = listCreate(); //如果buf 数组溢出,则使用reply链表
c->reply_bytes = 0; //reply链表中对象总共的字节数
c->obuf_soft_limit_reached_time = 0;
listSetFreeMethod(c->reply,freeClientReplyValue);
listSetDupMethod(c->reply,dupClientReplyValue);
if (fd != -1) linkClient(c);
return c;
}

处理数据

redis通过aeProcessEvents函数处理各种事件,首先它会调用aeApiPoll函数通过多路复用函数来检查已经触发的事件,并将已经触发事件的文件描述符,事件类型赋值给eventLoop->fired。然后根据事件触发的类型,调用之前注册的函数。

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents; int j;
struct timeval tv, *tvp; tvp = NULL; /* wait forever */ 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; if (fe->mask & mask & AE_READABLE) {
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
} if (fe->mask & mask & AE_WRITABLE) {
if (!fired || fe->wfileProc != fe->rfileProc) {
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
} processed++;
} return processed; /* return the number of processed file/time events */
}

如果此时有来自客户端的数据,那么将会触发AE_READABLE事件,调用readQueryFromClient函数。默认情况一次读取16KB,除非上次已经读取过数据,并且数据量较大,一行长度超过32KB。(超过32KB则会对其优化,避免了字符串的拷贝,代价是多了几次read调用)。

如果超过32KB,并且剩余长度小于16KB,那么一次读取剩余该行长度的值。这是因为TCP接受的数据不一定是完整的数据,如果是PROTO_REQ_MULTIBULK多行请求,并且数据量过大,在redis开始处理请求前需要接收全部的数据,等待的时间过长,并且解析完毕之后,执行命令的时间和下发数据的长度也会影响性能。建议一次请求不超过16KB,但这16KB中还包含着*/r/n等格式符号,因此请求的数据量还要再小一些,才能保证服务端尽可能在一次接收数据的过程中完成命令的解析。

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
client *c = (client*) privdata;
int nread, readlen;
size_t qblen; readlen = PROTO_IOBUF_LEN;//1024*16 bytes if (c->reqtype == PROTO_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
&& c->bulklen >= PROTO_MBULK_BIG_ARG)
{
ssize_t remaining = (size_t)(c->bulklen+2)-sdslen(c->querybuf); //如果超过**32KB**,并且剩余长度小于**16KB**,那么一次读取剩余该行长度的值。
// 如果触发,则在processMultibulkBuffer可以直接使用现有的字符串避免了字符串的复制,代价是多调用了几次 read(2)函数。
if (remaining < readlen) readlen = remaining;
} qblen = sdslen(c->querybuf); c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
nread = read(fd, c->querybuf+qblen, readlen);
if (nread == -1) {
if (errno == EAGAIN) {
return;
} else {
serverLog(LL_VERBOSE, "Reading from client: %s",strerror(errno));
freeClient(c);
return;
}
} else if (nread == 0) {
serverLog(LL_VERBOSE, "Client closed connection");
freeClient(c);
return;
} sdsIncrLen(c->querybuf,nread); processInputBuffer(c); }

接着就会进入processInputBuffer函数,此时数据可能全部抵达,也可能部分抵达。processInputBuffer函数的主要功能是将,client->querybuff里面的数据解析,并转化为client->argcclient->argv的数据。如果数据全部抵达,那么接着会进入到processComand函数,查找命令表,执行命令并返回数据给客户的。如果数据部分抵达,但是一行的数据内容抵达,那么该行数据会被解析到client->argcclient->argv中去。

返回数据结果

在这里我们假设客户端输入的字符串是quitprocessComand函数会调用addReply函数将当前的client加入到clients_pending_write链表中。

再将存储OK字符串的对象添加到缓冲区,服务端返回给客户端的编码类型只可能是字符型或者是INT型。首先redis会尝试将结果添加到缓冲区,缓冲区的大小默认16KB,并且不能通过配置更改。如果缓冲区会溢出,那么redis会将数据添加到client->reply链表中。

void addReply(client *client, robj *obj) {
if (prepareClientToWrite(client) != C_OK) return; if (sdsEncodedObject(obj)) {
if (_addReplyToBuffer(client,obj->ptr,sdslen(obj->ptr)) != C_OK)
_addReplyObjectToList(client,obj);
} else if (obj->encoding == OBJ_ENCODING_INT) {
...
} else {
// serverPanic("Wrong obj->encoding in addReply()");
serverLog(LL_WARNING, "Wron obj->encoding in addReply()");
}
}

此时数据还没有返回给客户端,在redis进入下一次循环的时候,会调用beforeSleep函数将数据返回给客户端。

为什么redis不直接将数据返回给客户端呢?

源码的注释给出了答案:为了实现fsync=always的效果,将返回数据放在beforeSleep中,可以通过AOF持久后,再返回给客户端结果。

 /* For the fsync=always policy, we want that a given FD is never
* served for reading and writing in the same event loop iteration,
* so that in the middle of receiving the query, and serving it
* to the client, we'll call beforeSleep() that will do the
* actual fsync of AOF to disk. AE_BARRIER ensures that. */

beforeLoop会接着调用handleClientsWithPendingWrites函数来处理有缓存数据的clientwriteToClient函数会将buf中和reply链表中的数据全部发送给客户端,如果实际发送的数据小于应当发送的数据,则表示系统缓存区空间不够,或者文件描述符在中途被占用,那么redis会创建一个事件,当监听到文件描述符可读时,再将剩余数据写入。

int handleClientsWithPendingWrites(void) {
listIter li;
listNode *ln;
int processed = listLength(server.clients_pending_write); listRewind(server.clients_pending_write,&li);
while((ln = listNext(&li))) {
client *c = listNodeValue(ln);
c->flags &= ~CLIENT_PENDING_WRITE;
listDelNode(server.clients_pending_write,ln); /* Try to write buffers to the client socket. */
if (writeToClient(c->fd,c,0) == C_ERR) continue; /* If after the synchronous writes above we still have data to
* output to the client, we need to install the writable handler. */
if (clientHasPendingReplies(c)) {
int ae_flags = AE_WRITABLE;
if (aeCreateFileEvent(server.el, c->fd, ae_flags,
sendReplyToClient, c) == AE_ERR)
{
freeClientAsync(c);
}
}
}
return processed;
}

在写完数据后,发现客户端有被标记CLIENT_CLOSE_AFTER_REPLY,那么将会释放客户端的资源。

if (c->flags & CLIENT_CLOSE_AFTER_REPLY) {
freeClient(c);
return C_ERR;
}

参考文献

accept函数

《Redis设计与实现》

自顶向下redis4.0(2)文件事件与客户端的更多相关文章

  1. 自顶向下redis4.0(4)时间事件与expire

    redis4.0的时间事件与expire 目录 redis4.0的时间事件与expire 简介 正文 时间事件注册 时间事件触发 expire命令 删除过期键值 被动删除 主动删除/定期删除 参考文献 ...

  2. 自顶向下redis4.0(1)启动

    redis4.0的启动流程 目录 redis4.0的启动流程 简介 正文 全局server对象 初始化配置 初始化服务器 事件主循环 参考文献 简介 redis 在接收客户端连接之前,大概做了以下几件 ...

  3. 自顶向下redis4.0(5)持久化

    redis4.0的持久化 目录 redis4.0的持久化 简介 正文 rdb持久化 save命令 bgsave命令 rdb定期保存数据 进程结束保存数据 aof持久化 数据缓冲区 刷新数据到磁盘 ap ...

  4. 自顶向下redis4.0(3)命令与dict

    redis4.0的命令 简介 目录 redis4.0的命令 简介 正文 redisCommand与redisCommandTable 初始化命令 执行命令 set指令与字典 参考文献 正文 redis ...

  5. Redis4.0.0 安装及配置 (Linux — Centos7)

    本文中的两个配置文件可在这里找到 操作系统:Linux Linux发行版:Centos7 安装 下载地址,点这里Redis4.0.0.tar.gz 或者使用命令: wget http://downlo ...

  6. Redis4.0 Cluster — Centos7

    本文版权归博客园和作者吴双本人共同所有 转载和爬虫请注明原文地址 www.cnblogs.com/tdws 一.基础安装 wget http://download.redis.io/releases/ ...

  7. centos7 安装 redis-4.0.9

    下载地址:https://redis.io/download 下载 安装: $ wget http://download.redis.io/releases/redis-4.0.9.tar.gz $ ...

  8. redis4.0.13主从、哨兵、集群3种模式的 Server端搭建、启动、验证

    本文使用的是redis-4.0.13.tar.gz版本. 两个centos7系统虚拟机:192.168.10.140.192.168.10.150 redis各版本下载地址:http://downlo ...

  9. redis-4.0.8 配置文件解读

    # Redis configuration file example.## Note that in order to read the configuration file, Redis must ...

随机推荐

  1. 计算思维(美国CMU周以真教授)

    博主注:GIScience会议是国际上最为著名的地理信息系统领域的国际会议,自2000年起,每两年举办一次,GIScience 2008会议邀请了美国卡内基-梅隆大学(CMU)计算机系华裔教授周以真博 ...

  2. 凭借着这份面经,我拿下了字节,美团的offer!

    最近经常有粉丝私信问我问了一些诸如秋招该怎么复习的问题,我就想顺便把回答整理发一发.我也是把之前面试的一些经历经验和身边的人面试的经验总结了一下放在下面. 前期准备规划: 如果秋招的话一般过年回来就可 ...

  3. Redis多线程原理详解

    本篇文章为你解答一下问题: 0:redis单线程的实现流程是怎样的? 1:redis哪些地方用到了多线程,哪些地方是单线程? 2:redis多线程是怎么实现的? 3:redis多线程是怎么做到无锁的? ...

  4. 痞子衡嵌入式:在IAR开发环境下为工程开启CRC完整性校验功能的方法

    大家好,我是痞子衡,是正经搞技术的痞子.今天痞子衡给大家分享的是在IAR开发环境下为工程开启CRC完整性校验功能的方法. CRC校验在嵌入式领域里的应用非常广,比如在通信领域,CRC检验值可以作为数据 ...

  5. java中String类的使用

    一.Strng类的概念 String类在我们开发中经常使用,在jdk1.8版本之前(包括1.8),String类的底层是一个char类型的数组,1.8版本之后是byte类型的数组,正是因为String ...

  6. ios开发中如何调用苹果自带地图导航

    前段时间一直在赶项目,在外包公司工作就是命苦,天天加班不说,工作都是和工期合同挂钩的,稍微逾期就有可能被扣奖金,不谈这些伤脑筋的事情了,让我们说说iOS开发中如何调用苹果手机自带的地图. 学习如逆水行 ...

  7. 放进你的收藏夹吃灰!Linux 运维必备的 40 个命令总结

    1.删除0字节文件 find -type f -size 0 -exec rm -rf {} ; 2.查看进程 按内存从大到小排列 PS -e -o "%C : %p : %z : %a&q ...

  8. How tomcat works(深入剖析tomcat)生命周期Lifecycle

    How Tomcat Works (6)生命周期Lifecycle 总体概述 这一章讲的是tomcat的组件之一,LifeCycle组件,通过这个组件可以统一管理其他组件,可以达到统一启动/关闭组件的 ...

  9. Error:Execution failed for task ':app:mergeDebugResources'. > Error: Some file crunching failed, see logs for details

    android studio中的资源文件命名是不能带有数字的,因为会与R类的资源ID起冲突,所以编译就发生了错误.

  10. 新手上路A4——多JDK环境变量的配置

    目录 配置单个JDK的方法 配置2+JDK的方法 方法 补充 检查JDK版本是否切换成功 前面讲了如何选择Java版本. 以及JDK8和JDK11的下载安装配置 有想法的人就开始发动他们优秀的小脑袋瓜 ...