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. txt文件覆盖恢复

    1.txt文件恢复到之前保存的版本 2.电脑未重启 方式:如果你使用系统还原可以用"还原以前的版本"功能来轻松找回. 右击.txt文件-还原以前的版本-选择时间点-还原

  2. laydate 使用

    官网 http://www.layui.com/doc/modules/laydate.html 参考 http://www.cnblogs.com/fengpingfan/p/4660273.htm ...

  3. kali 系列学习04 - 漏洞扫描

    一.比较三类漏洞扫描工具 1.Rapid7 Nexpose 适合较大网络 2.Nessus 更经济,可以申请个人版,搞之后硬盘占用达到20G 以上2个是商业软件,使用容易上手,输入IP地址就能完成所有 ...

  4. 还不懂java类加载机制的,建议看下这份阿里技术官总结的笔记!

    类加载机制 把class文件加载到内存,并对数据进行校验,准备,解析,初始化,形成可以被虚拟机直接使用的字节码 类加载的时机(触发类的初始化) 使用new关键字实例化对象 读取一个类的静态代码块 使用 ...

  5. 微课制作软件Camtasia,来为视频添加预设动画效果

    之前已介绍过使用微课制作软件Camtasia为视频添加"缩放和平移"动画的教程以及"效果按钮"的使用. 此篇内容,我们就来介绍使用录像编辑软件--Camtasi ...

  6. 常用命令合集『Postgres、Redis、Docker等等』每周更新,建议收藏备用

    Command CMD POSTGRES 进入数据库命令行 psql -U 用户名 -d 数据库名 psql -U example -d exampledb 导出数据库 pg_dump -U 用户名 ...

  7. php 缩略图封装的方法

    /** * PHP生成缩略图 * @param $basepath /原文件地址 * @param $des_w /缩略图的宽 * @param $des_h /缩略图的高 * @param $sty ...

  8. Redis 基础数据结构之一:string(字符串)

    Redis 有 5 种基础数据结构,分别为:string (字符串).list (列表).set (集合).hash (哈希) 和 zset (有序集合),Redis存储数据的结构是键值对形式的. 首 ...

  9. Access数据库简介

    一.Access数据库的简介 1.microsoft office access是由微软发布的关联式数据库管理系统.它结合了 microsoft jet database engine 和 图形用户界 ...

  10. Error:Execution failed for task ':app:compileDebugAidl'. > java.lang.IllegalStateException: aidl is missing from '/Users/renguodong/Library/Android/sdk/build-tools/26.0.2/aidl'

    错误信息:Error:Execution failed for task ':app:compileDebugAidl'. > java.lang.IllegalStateException: ...