TODO
本文内容
定时器处理非活动连接模块,主要分为两部分,其一为定时方法与信号通知流程,其二为定时器及其容器设计、定时任务的处理。
本篇对第二部分进行介绍,具体的涉及到定时器设计、容器设计、定时任务处理函数和使用定时器。
定时器设计
,将连接资源和定时事件等封装起来,具体包括连接资源、超时时间和回调函数,这里的回调函数指向定时事件。
定时器容器设计
,将多个定时器串联组织起来统一处理,具体包括升序链表设计。
定时任务处理函数
,该函数封装在容器类中,具体的,函数遍历升序链表容器,根据超时时间,处理对应的定时器。
代码分析-使用定时器
,通过代码分析,如何在项目中使用定时器。
定时器设计
项目中将连接资源、定时事件和超时时间封装为定时器类,具体的,
- 连接资源包括客户端套接字地址、文件描述符和定时器
- 定时事件为回调函数,将其封装起来由用户自定义,这里是删除非活动socket上的注册事件,并关闭
- 定时器超时时间 = 浏览器和服务器连接时刻 + 固定时间(TIMESLOT),可以看出,定时器使用绝对时间作为超时值,这里alarm设置为5秒,连接超时为15秒。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
|
class util_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; };
|
定时事件,具体的,从内核事件表删除事件,关闭文件描述符,释放连接资源。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
void cb_func(client_data *user_data) { epoll_ctl(epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0); assert(user_data);
close(user_data->sockfd);
http_conn::m_user_count--; }
|
定时器容器设计
项目中的定时器容器为带头尾结点的升序双向链表,具体的为每个连接创建一个定时器,将其添加到链表中,并按照超时时间升序排列。执行定时任务时,将到期的定时器从链表中删除。
从实现上看,主要涉及双向链表的插入,删除操作,其中添加定时器的事件复杂度是O(n),删除定时器的事件复杂度是O(1)。
升序双向链表主要逻辑如下,具体的,
- 创建头尾节点,其中头尾节点没有意义,仅仅统一方便调整
- add_timer函数,将目标定时器添加到链表中,添加时按照升序添加
- 若当前链表中只有头尾节点,直接插入
- 否则,将定时器按升序插入
- adjust_timer函数,当定时任务发生变化,调整对应定时器在链表中的位置
- 客户端在设定时间内有数据收发,则当前时刻对该定时器重新设定时间,这里只是往后延长超时时间
- 被调整的目标定时器在尾部,或定时器新的超时值仍然小于下一个定时器的超时,不用调整
- 否则先将定时器从链表取出,重新插入链表
- del_timer函数将超时的定时器从链表中删除
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158
| class sort_timer_lst { public: sort_timer_lst() : head( NULL ), tail( NULL ) {} ~sort_timer_lst() { util_timer* tmp = head; while( tmp ) { head = tmp->next; delete tmp; tmp = head; } }
void 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 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 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; }
private: void 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; } }
private: util_timer* head; util_timer* tail; };
|
定时任务处理函数
使用统一事件源,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器。
具体的逻辑如下,
- 遍历定时器升序链表容器,从头结点开始依次处理每个定时器,直到遇到尚未到期的定时器
- 若当前时间小于定时器超时时间,跳出循环,即未找到到期的定时器
- 若当前时间大于定时器超时时间,即找到了到期的定时器,执行回调函数,然后将它从链表中删除,然后继续遍历
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| void 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 ) { head->prev = NULL; } delete tmp; tmp = head; } }
|
代码分析-如何使用定时器
服务器首先创建定时器容器链表,然后用统一事件源将异常事件,读写事件和信号事件统一处理,根据不同事件的对应逻辑使用定时器。
具体的,
- 浏览器与服务器连接时,创建该连接对应的定时器,并将该定时器添加到链表上
- 处理异常事件时,执行定时事件,服务器关闭连接,从链表上移除对应定时器
- 处理定时信号时,将定时标志设置为true
- 处理读事件时,若某连接上发生读事件,将对应定时器向后移动,否则,执行定时事件
- 处理写事件时,若服务器通过某连接给浏览器发送数据,将对应定时器向后移动,否则,执行定时事件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141
| void timer_handler() { timer_lst.tick(); alarm(TIMESLOT); }
static sort_timer_lst timer_lst;
client_data *users_timer = new client_data[MAX_FD];
bool timeout = false;
alarm(TIMESLOT);
while (!stop_server) { int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1); if (number < 0 && errno != EINTR) { break; }
for (int i = 0; i < number; i++) { int sockfd = events[i].data.fd;
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); 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)) { cb_func(&users_timer[sockfd]); util_timer *timer = users_timer[sockfd].timer; if (timer) { timer_lst.del_timer(timer); } }
else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN)) { }
else if (events[i].events & EPOLLIN) { util_timer *timer = users_timer[sockfd].timer; if (users[sockfd].read_once()) { pool->append(users + sockfd);
if (timer) { time_t cur = time(NULL); timer->expire = cur + 3 * TIMESLOT; timer_lst.adjust_timer(timer); } } else { 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()) { if (timer) { time_t cur = time(NULL); timer->expire = cur + 3 * TIMESLOT; timer_lst.adjust_timer(timer); } } else { cb_func(&users_timer[sockfd]); if (timer) { timer_lst.del_timer(timer); } } } } if (timeout) { timer_handler(); timeout = false; } }
|
有小伙伴问,连接资源中的address是不是有点鸡肋?
确实如此,项目中虽然对该变量赋值,但并没有用到。类似的,可以对比HTTP类中address属性,只在日志输出中用到。
但不能说这个变量没有用,因为我们可以找到客户端连接的ip地址,用它来做一些业务,比如通过ip来判断是否异地登录等等。
如果本文对你有帮助,阅读原文
star一下服务器项目,我们需要你的星星^_^.
完。