主要分为两个部分,其一是定时方法与信号通知流程,其二为定时器及其容器设计、定时任务处理
非活跃连接:客户端与服务端简历建立连接后,长时间不进行数据交换,导致占用服务端的文件描述符,导致连接资源浪费。
定时事件:固定一段时间之后触发某段代码,这段代码完成一个事件(关闭连接文件描述符,释放连接资源)。
定时器:利用结构体或其他形式,将多种定时器事件进行封装。将该定时事件与连接资源封装为一个结构体定时器。
定时器容器:将多个定时器组合起来,便于对定时事件统一管理,这里使用的是升序双向链表。
常见的定时器容器:
优先级队列:
- 优点:能够自动对定时器按到期时间进行排序,最先到期的定时器始终位于队列的前面,方便快速获取和处理最近的定时器。
- 缺点:插入和删除操作的时间复杂度较高,对于大量定时器的频繁插入和删除操作可能会影响性能。
红黑树:
- 优点:具有较好的查找性能,查找特定到期时间的定时器的时间复杂度为 O(logN),适用于需要快速定位定时器的场景。
- 缺点:插入和删除操作的时间复杂度较高,相对于其他容器来说,实现红黑树的代码复杂性较高。
堆:
- 优点:具有较好的插入和删除性能,插入和删除操作的时间复杂度为 O(logN),并且堆可以保持最小到期时间的定时器位于堆顶,方便获取最近的定时器。
- 缺点:查找特定到期时间的定时器的性能较差,需要进行线性遍历,时间复杂度为 O(N)。
链表:
- 优点:插入和删除操作的时间复杂度为 O(1),在定时器触发时可以直接遍历链表找到到期的定时器进行处理。
- 缺点:查找特定到期时间的定时器的性能较差,需要进行线性遍历,时间复杂度为 O(N)。对于大量定时器的查找操作,性能较差。
定时器设计
将连接资源、定时事件和超时时间封装为定时器类,具体的:
-
连接资源包括客户端套接字地址、文件描述符和定时器
//连接资源
struct client_data
{
//客户端socket地址
sockaddr_in address;
//socket文件描述符
int sockfd;
//定时器
util_timer *timer;
};
//定时器类
class util_timer
{
public:
util_timer() : prev(NULL), next(NULL) {}
public:
//超时时间
time_t expire;
//回调函数
void (*cb_func)(client_data *);
//连接资源
client_data *user_data;
//前向定时器
util_timer *prev;
//后继定时器
util_timer *next;
};
-
定时事件为回调函数,将其封装起来由用户自定义,这里是删除非活动socket上的注册事件,并关闭
//定时器回调函数,删除非活动连接在socket上的注册事件,并关闭
void cb_func(client_data *user_data)
{
//删除非活动连接在socket上的注册事件
epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
assert(user_data);
//关闭文件描述符
close(user_data->sockfd);
//减少连接数
http_conn::m_user_count--;
LOG_INFO("close fd %d", user_data->sockfd);
Log::get_instance()->flush();
}
-
定时器超时时间 = 浏览器和服务器连接时刻 + 固定时间(TIMESLOT),可以看出,定时器使用绝对时间作为超时值,这里alarm设置为5秒,连接超时为15秒
定时器容器设计
项目中的定时器容器为带头尾结点的升序双向链表,具体的为每个连接创建一个定时器,将其添加到链表中,并按照超时时间升序排列。执行定时任务时,将到期的定时器从链表中删除。
从实现上看,主要涉及双向链表的插入,删除操作,其中添加定时器的事件复杂度是O(n),删除定时器的事件复杂度是O(1)
升序双向链表主要逻辑如下,具体的:
-
创建头尾节点,其中头尾节点没有意义,仅仅统一方便调整
-
add_timer函数,将目标定时器添加到链表中,添加时按照升序添加
-
若当前链表中只有头尾节点,直接插入
-
否则,将定时器按升序插入
-
-
adjust_timer函数,当定时任务发生变化,调整对应定时器在链表中的位置
-
客户端在设定时间内有数据收发,则当前时刻对该定时器重新设定时间,这里只是往后延长超时时间
-
被调整的目标定时器在尾部,或定时器新的超时值仍然小于下一个定时器的超时,不用调整
-
否则先将定时器从链表取出,重新插入链表
-
-
del_timer函数将超时的定时器从链表中删除
-
常规双向链表删除结点
-
定时任务处理函数
使用统一事件源,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器。
具体的逻辑如下,
-
遍历定时器升序链表容器,从头结点开始依次处理每个定时器,直到遇到尚未到期的定时器
-
若当前时间小于定时器超时时间,跳出循环,即未找到到期的定时器
-
若当前时间大于定时器超时时间,即找到了到期的定时器,执行回调函数,然后将它从链表中删除,然后继续遍历
//定时任务处理函数
void tick()
{
if (!head)
{
return;
}
//printf( "timer tick\n" );
LOG_INFO("%s", "timer tick");
Log::get_instance()->flush();
//获取当前时间
time_t cur = time(NULL);
util_timer *tmp = head;
//遍历定时器链表
while (tmp)
{
//链表容器为升序排列
//当前时间小于定时器的超时时间,后面的定时器也没有到期
if (cur < tmp->expire)
{
break;
}
//当前定时器到期,则调用回调函数,执行定时事件
tmp->cb_func(tmp->user_data);
//将处理后的定时器从链表容器中删除,并重置头s结点
head = tmp->next;
if (head)
{
head->prev = NULL;
}
delete tmp;
tmp = head;
}
}
使用定时器
-
浏览器与服务器连接时,创建该连接对应的定时器,并将该定时器添加到链表上
-
处理异常事件时,执行定时事件,服务器关闭连接,从链表上移除对应定时器
-
处理定时信号时,将定时标志设置为true
-
处理读事件时,若某连接上发生读事件,将对应定时器向后移动,否则,执行定时事件
-
处理写事件时,若服务器通过某连接给浏览器发送数据,将对应定时器向后移动,否则,执行定时事件
//创建定时器容器链表
static sort_timer_lst timer_lst;
//定时处理任务,重新定时以不断触发SIGALRM信号
void timer_handler()
{
timer_lst.tick();
alarm(TIMESLOT);
}
//创建连接资源数组
client_data *users_timer = new client_data[MAX_FD];
//超时默认为False
bool timeout = false;
//alarm定时触发SIGALRM信号
alarm(TIMESLOT);
while (!stop_server)
{
//等待所监控文件描述符上有事件的产生
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
LOG_ERROR("%s", "epoll failure");
break;
}
//处理新到的客户连接
if (sockfd == listenfd)
{
//初始化客户端连接地址
struct sockaddr_in client_address;
socklen_t client_addrlength = sizeof(client_address);
//该连接分配的文件描述符
int connfd = accept(listenfd, (struct sockaddr *)&client_address, &client_addrlength);
//初始化client_data数据
//创建定时器,设置回调函数和超时时间,绑定用户数据,将定时器添加到链表中
users_timer[connfd].address = client_address;
users_timer[connfd].sockfd = connfd;
//创建定时器临时变量
util_timer *timer = new util_timer;
//设置定时器对应的连接资源
timer->user_data = &users_timer[connfd];
//设置回调函数
timer->cb_func = cb_func;
time_t cur = time(NULL);
//设置绝对超时时间
timer->expire = cur + 3 * TIMESLOT;
//创建该连接对应的定时器,初始化为前述临时变量
users_timer[connfd].timer = timer;
//将该定时器添加到链表中
timer_lst.add_timer(timer);
}
//处理异常事件
else if (events[i].events & (EPOLLRDHUP | EPOLLHUP | EPOLLERR))
{
//服务器端关闭连接,移除对应的定时器
util_timer *timer = users_timer[sockfd].timer;
timer->cb_func(&users_timer[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
//处理信号 读到管道信息
else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
{
int sig;
char signals[1024];
ret = recv(pipefd[0], signals, sizeof(signals), 0);
if (ret == -1)
{
continue;
}
else if (ret == 0)
{
continue;
}
else
{
for (int i = 0; i < ret; ++i)
{
switch (signals[i])
{
case SIGALRM:
{
// timeout变量标记有定时任务需要处理 但不立即处理
//有限处理IO
timeout = true;
break;
}
case SIGTERM:
{
stop_server = true;
}
}
}
}
}
//处理客户连接上接收到的数据 有数据到达
else if (events[i].events & EPOLLIN)
{
util_timer *timer = users_timer[sockfd].timer;
//读入对应缓冲区
if (users[sockfd].read_once())
{
LOG_INFO("deal with the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));
Log::get_instance()->flush();
//若监测到读事件,将该事件放入请求队列
pool->append(users + sockfd);
//若有数据传输,则将定时器往后延迟3个单位
//并对新的定时器在链表上的位置进行调整
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT; //更新时间
LOG_INFO("%s", "adjust timer once");
Log::get_instance()->flush();
timer_lst.adjust_timer(timer); //修改位置
}
}
else
{
//服务器端关闭连接,移除对应的定时器
timer->cb_func(&users_timer[sockfd]);
if (timer)
{
timer_lst.del_timer(timer); //删除
}
}
}
//有数据需要传输
else if (events[i].events & EPOLLOUT)
{
util_timer *timer = users_timer[sockfd].timer;
if (users[sockfd].write())
{
LOG_INFO("send data to the client(%s)", inet_ntoa(users[sockfd].get_address()->sin_addr));
Log::get_instance()->flush();
//若有数据传输,则将定时器往后延迟3个单位
//并对新的定时器在链表上的位置进行调整
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
LOG_INFO("%s", "adjust timer once");
Log::get_instance()->flush();
timer_lst.adjust_timer(timer);
}
}
else
{
timer->cb_func(&users_timer[sockfd]);
if (timer)
{
timer_lst.del_timer(timer);
}
}
}
//最后处理定时事件 I/O事件有更高优先级
if (timeout)
{
timer_handler();
timeout = false;
}
}