TinyWebserver学习(8)-定时器
定时器
原理解析
如果一个客户端与服务器长时间连接但是之间无资源的交互,那么就会浪费占据的服务器的资源,在这种情况下,服务器就会采取一种手段来检测这种无意义的连接,并对这些连接进行处理,如超过一定的时间无反应则对其进行清除;
除了处理非活跃的连接之外,服务器还有一些定时事件,比如关闭文件描述符等。服务器程序通常管理着众多定时事件, 因此有效地组织这些定时事件, 使之能在预期的时间点被触发且不影响服务器的主要逻辑, 对于服务器的性能有着至关重要的影响。 为实现这些功能,服务器就需要为各事件分配一个定时器。
为此,我们要将每个定时事件分别封装成定时器,并使用某种容器类数据结构, 比如链表、排序链表和时间轮, 将所有定时器串联起来,以实现对定时事件的统一管理。 不过, 在讨论如何组织定时器之前, 我们先要介绍定时的方法。
Linux提供了三种定时方法, 它们是:
- socket选项SO_RCVTIMEO和SO_SNDTIMEO。
- SIGALRM信号。
- I/O复用系统调用的超时参数
在该项目中,采用的是SIGALRM信号来作为定时器的实现方法,首先每一个定时事件都处于一个升序链表上,通过alarm()函数周期性触发SIGALRM信号,而后信号回调函数利用管道通知主循环,主循环接收到信号之后对升序链表上的定时器进行处理,若查询到长时间无连续的事件,则将其删除。
定时器框架图:
在整个项目中主要就是通过维持一个双向的升序链表来实现定时器的增删查改的,然后定时器的结构包含客户端数据、超时时间、上/下节点指针。 然后程序中会设置一个定时发送器,每隔一段时间发送一个SIGARLM,其回调函数会通过pip管道向主线程eventloop发送信号,主线程就会执行相关函数来检测是否有超时的时间。
接下来具体的看一下:
在eventlisten()函数中,首先设置了一个alarm定时器来发送信号
....utils.addsig(SIGPIPE, SIG_IGN);utils.addsig(SIGALRM, utils.sig_handler, false);utils.addsig(SIGTERM, utils.sig_handler, false);alarm(TIMESLOT); //每隔一段时间向主循环发送一次SIGARLM信号//工具类,信号和描述符基础操作Utils::u_pipefd = m_pipefd;Utils::u_epollfd = m_epollfd;
dealclientdata()函数
...if (0 == m_LISTENTrigmode){int connfd = accept(m_listenfd, (struct sockaddr *)&client_address, &client_addrlength); //addr是储存客户端地址的变量,它的长度用指针len指向if (connfd < 0){LOG_ERROR("%s:errno is:%d", "accept error", errno);return false;}if (http_conn::m_user_count >= MAX_FD){utils.show_error(connfd, "Internal server busy");LOG_ERROR("%s", "Internal server busy");return false;}timer(connfd, client_address);}......
在主程序处理新连接的时候,最后会将新的连接与一个定时器绑定,代码如下:
void WebServer::timer(int connfd, struct sockaddr_in client_address)
{ //创建并初始化http_conn对象users[connfd].init(connfd, client_address, m_root, m_CONNTrigmode, m_close_log, m_user, m_passWord, m_databaseName);//初始化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;utils.m_timer_lst.add_timer(timer);//将定时器加入链表中
}
客户端数据和定时器结构如下所示:
struct client_data
{sockaddr_in address;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;
};
接着再回到主线程中,如果主线程接收到了定时器发送的信号,则会执行dealwithsignal()函数,如下:
eventloop()函数
...
//处理信号else if ((sockfd == m_pipefd[0]) && (events[i].events & EPOLLIN)){bool flag = dealwithsignal(timeout, stop_server);if (false == flag)LOG_ERROR("%s", "dealclientdata failure");}.........if (timeout){utils.timer_handler(); //重复发送SIGRALM信号LOG_INFO("%s", "timer tick");timeout = false;}
bool WebServer::dealwithsignal(bool &timeout, bool &stop_server)
{int ret = 0;int sig;char signals[1024];ret = recv(m_pipefd[0], signals, sizeof(signals), 0);//接受信号if (ret == -1){return false;}else if (ret == 0){return false;}else{for (int i = 0; i < ret; ++i){switch (signals[i]){case SIGALRM:{timeout = true;break;}case SIGTERM:{stop_server = true;break;}}}}return true;
}
如果是定时器发送的信号,则会将timeout设置为true,然后在eventloop最后,就会执行time_handler()函数。
//定时处理任务,重新定时以不断触发SIGALRM信号
void Utils::timer_handler()
{m_timer_lst.tick();alarm(m_TIMESLOT);
}
tick()函数主要就是检测整个链表有没有超时的事件,如果有则将其删除,因为链表是升序链表,所以从头节点开始检测,如果头节点都没有超时,则整个链表都无超时,如果头节点超时了,则将其删除,并接着往下检查,知道都没有事件超时。
void sort_timer_lst::tick()//从头到尾检测有没有超时任务
{if (!head){return;}time_t cur = time(NULL);//当前时间util_timer *tmp = head;while (tmp){if (cur < tmp->expire)//如果首节点没有超时则跳出循环(因为为升序链表,所以首节点最大){break;}tmp->cb_func(tmp->user_data);//调用回调函数,处理定时任务head = tmp->next;if (head) //如果是头节点,则将头节点的上一个设置为NULL{head->prev = NULL;}delete tmp;tmp = head;}
}
那现在有一个问题,超时时间应该是可以改变的,如果客户端和服务器有互动的话,超时时间就应该更新,那么再回到主线程,当事件为read/write的时候,我们可以看相应的处理程序dealwithread()/dealwithwrite()
void WebServer::dealwithread(int sockfd)
{util_timer *timer = users_timer[sockfd].timer;//reactor(反应堆),就是IO多路复用,收到事件后,根据事件类型分配给某个线程if (1 == m_actormodel){if (timer){adjust_timer(timer);//调整事件}//若监测到读事件,将该事件放入请求队列m_pool->append(users + sockfd, 0); //users是一个数组指针,sockfd是索引,因此这个表示的就是当前处理的客户端的对象//stat:0表示read事件,1表示write事件while (true){if (1 == users[sockfd].improv){if (1 == users[sockfd].timer_flag){deal_timer(timer, sockfd);users[sockfd].timer_flag = 0;}users[sockfd].improv = 0;break;}}}
adjust_timer()函数
会更新超时时间expire
void WebServer::adjust_timer(util_timer *timer)
{time_t cur = time(NULL);timer->expire = cur + 3 * TIMESLOT;utils.m_timer_lst.adjust_timer(timer);LOG_INFO("%s", "adjust timer once");
}
最后,看一下链表的增删查改的一些函数,也都比较好理解:
对于add_timer,这里有一个重载
void sort_timer_lst::add_timer(util_timer *timer)
{if (!timer){return;}if (!head){head = tail = timer;return;}if (timer->expire < head->expire)//如果加入的新的定时器的超时时间小于链表头节点的时间,则将头指针指向它{timer->next = head;head->prev = timer;head = timer;return;}add_timer(timer, head);//将新的定时器插入到链表中的合适位置(升序)
}
void sort_timer_lst::add_timer(util_timer *timer, util_timer *lst_head)
{util_timer *prev = lst_head;util_timer *tmp = prev->next;while (tmp){if (timer->expire < tmp->expire){prev->next = timer;timer->next = tmp;tmp->prev = timer;timer->prev = prev;break;}prev = tmp;tmp = tmp->next;}if (!tmp){prev->next = timer;timer->prev = prev;timer->next = NULL;tail = timer;}
}
adjust_timer()函数主要就是把原来的删除了,然后把新的重新加入到链表中
void sort_timer_lst::adjust_timer(util_timer *timer)//将定时器节点重新断开,然后重新插入定时器链表中
{if (!timer){return;}util_timer *tmp = timer->next;if (!tmp || (timer->expire < tmp->expire)){return;}if (timer == head){head = head->next;head->prev = NULL;timer->next = NULL;add_timer(timer, head);}else{timer->prev->next = timer->next;timer->next->prev = timer->prev;add_timer(timer, timer->next);}
}
void sort_timer_lst::del_timer(util_timer *timer)
{if (!timer){return;}if ((timer == head) && (timer == tail)){delete timer;head = NULL;tail = NULL;return;}if (timer == head){head = head->next;head->prev = NULL;delete timer;return;}if (timer == tail){tail = tail->prev;tail->next = NULL;delete timer;return;}timer->prev->next = timer->next;timer->next->prev = timer->prev;delete timer;
}
这些就是定时器的整个内容了