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. docker 国内源切换加速

    阿里云比较好: 地址: https://cr.console.aliyun.com/cn-hangzhou/instances/mirrors

  2. php(tp5)生成条形码

    因为公司业务需要,研究了一下条形码 1.下载barcodegen扩展包 官网地址:https://www.barcodebakery.com 2.下载完后解压至 extend 文件夹里面,然后复制以下 ...

  3. (msf使用)msfconsole - meterpreter

    [msf] msfconsole meterpreter 对于这款强大渗透测试框架,详情介绍可看这里:metasploit 使用教程 对于msfconsole, Kali Linux 自带.只需用命令 ...

  4. 深度分析:Java并发编程之线程池技术,看完面试这个再也不慌了!

    线程池的好处 Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池.在开发过程中,合理地使用线程池,相对于单线程串行处理(Serial Processing ...

  5. 如何在IDM中设置代理服务器?

    很多时候,大家下载文件都是在国外的一些网站上进行下载,这样不可免会受到自身国内网络的限制,另一方面下载源为避免服务器带宽占用过多而限制下载速率,这就会导致文件下载极慢,甚至几KB每秒. 这种情况是不是 ...

  6. MathType怎么写分段函数?

    分段函数是数学里面特有的一种函数,它是对于自变量x的不同的取值范围,有着不同的解析式的函数.它的特点就是有一个大括号,然后有至少2个函数解析式,写这样的函数离不开专业的公式编辑器,下面就来学习具体编辑 ...

  7. 常见的名片尺寸如何在CorelDRAW预设

    说到名片想必大家肯定不陌生,是我们生活中随处可见的物品,也是商家宣传必不可少的印刷物料.那么名片的尺寸是多少?我们做名片的时候该如何把握好名片的尺寸呢?在CDR中有专门的名片尺寸,下面小编就为大家简单 ...

  8. mac实用软件推荐 mac好用的软件

    终于入手了梦寐以求的苹果电脑,但却发现其操作系统与Windows大相径庭!不会使用怎么办?不用担心,我们可以借助软件的力量.一款实用的Mac软件不仅能够使你的工作效率显著提高,同时它还能帮助你更快地熟 ...

  9. windows创建隐藏用户的powershell脚本

    通过保存并重新注册已删除用户的注册表的方式来隐藏用户,未登录时登陆界面不可见,登陆后可见 方法详情见: https://www.k0rz3n.com/2018/06/26/windows%E6%B8% ...

  10. python批量爬取猫咪图片

    不多说直接上代码 首先需要安装需要的库,安装命令如下 pip install BeautifulSoup pip install requests pip install urllib pip ins ...