TinywebServer代码详解– 定时器处理非活动连接-下(8)

该blog内容转自:最新版Web服务器项目详解 - 08 定时器处理非活动连接(下)

该blog对上述内容进行补充(在本人的角度)

结合此前记录的blog一起学习:牛客WebServer项目实战(点击跳转)

原项目地址(点击跳转)

博主添加注释后项目地址(点击跳转)



一、基础知识

1.本文内容

定时器处理非活动连接模块,主要分为两部分,其一为定时方法与信号通知流程,其二为定时器及其容器设计、定时任务的处理

本篇对第二部分进行介绍,具体的涉及到定时器设计、容器设计、定时任务处理函数和使用定时器

定时器设计

  • 将连接资源和定时事件等封装起来,具体包括连接资源、超时时间和回调函数,这里的回调函数指向定时事件

定时器容器设计

  • 将多个定时器串联组织起来统一处理,具体包括升序链表设计

定时任务处理函数

  • 该函数封装在容器类中,具体的,函数遍历升序链表容器,根据超时时间,处理对应的定时器

代码分析-使用定时器

  • 通过代码分析,如何在项目中使用定时器



二、代码分析

1.定时器设计

项目中将连接资源定时事件超时时间封装为定时器类,具体的

连接资源

  • 包括客户端套接字地址、文件描述符和定时器

定时事件

  • 为回调函数,将其封装起来由用户自定义,这里是删除非活动socket上的注册事件,并关闭

定时器超时时间

  • 定时器超时时间 = 浏览器和服务器连接时刻 + 固定时间(TIMESLOT),可以看出,定时器使用绝对时间作为超时值,这里alarm设置为5秒,连接超时为15

timer/lst_timer.h

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
// 连接资源结构体成员需要用到定时器类
// 因此定时器类,需要前向声明
class util_timer;


// 连接资源
struct client_data
{
// 客户端socket地址
sockaddr_in address;
// socket文件描述符
int sockfd;
// 定时器对象指针
util_timer *timer;
};


// 定时器类:每一个定时器均是双向链表上的一个节点
class util_timer
{
public:
// 构造函数
// 成员列表初始化的方式,将新创建的定时器对象的前向/后向节点都设置为NULL
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;
};

定时事件,具体的,从内核事件表删除事件,关闭文件描述符,释放连接资源

timer/lst_timer.h

1
2
// 定时器回调函数
void cb_func(client_data *user_data);

timer/lst_timer.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/*
* func: 定时器回调函数
* 从内核事件表删除事件,关闭文件描述符,释放连接资源
*/
class Utils;
void cb_func(client_data *user_data)
{
// 从epoll实例维护的事件表,删除非活动连接在socket上的注册事件
epoll_ctl(Utils::u_epollfd, EPOLL_CTL_DEL, user_data->sockfd, 0);
// assert断言函数,确保user_data指针是有效的
assert(user_data);
// 关闭文件描述符
close(user_data->sockfd);
// 减少连接数
http_conn::m_user_count--;
// ???? 为什么没有将http对象的m_sockfd设置为-1
}


2.定时器容器设计

项目中的定时器容器为带头尾结点的升序双向链表,具体的为每个连接创建一个定时器,将其添加到链表中,并按照超时时间升序排列。执行定时任务时,将到期的定时器从链表中删除

从实现上看,主要涉及双向链表的插入,删除操作,其中添加定时器的事件复杂度是O(n),删除定时器的事件复杂度是O(1)

升序双向链表的主要逻辑如下,具体的:

创建头尾节点,其中头尾节点没有意义,仅仅统一方便调整

add_timer函数,将目标定时器添加到链表中,添加时按照超时时间升序添加

  • 若当前链表中只有头尾节点,直接插入
  • 否则,将定时器按升序插入(需要对双向链表进行遍历,找到合适的位置进行插入)

adjust_timer函数,当定时任务发生变化,调整对应定时器在链表中的位置

  • 客户端在设定时间内有数据收发,则当前时刻对该定时器重新设定时间,这里只是往后延长超时时间
  • 被调整的目标定时器在尾部,或定时器新的超时值仍然小于下一个定时器的超时,不用调整
  • 否则先将定时器从链表取出,重新插入链表

del_timer函数将超时的定时器从链表中删除

  • 常规双向链表删除结点

timer/lst_timer.h

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
// 定时器容器类--以双向链表实现,存储定时器对象
class sort_timer_lst
{
public:
// 构造函数创建--存储定时器对象的升序双向链表
sort_timer_lst();
// 析构函数--释放
~sort_timer_lst();

// 添加定时器,内部调用私有成员add_timer
void add_timer(util_timer *timer);
// 调整定时器,任务发生变化时,调整定时器在链表中的位置
void adjust_timer(util_timer *timer);
// 删除定时器
void del_timer(util_timer *timer);
// 定时任务处理函数
void tick();

private:
// 私有成员,被共有成员add_timer和adjust_timer调用
void add_timer(util_timer *timer,util_timer *lst_head);
// 双向链表的头尾节点
util_timer *head;
util_timer *tail;
};

timer/lst_timer.cpp

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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
/*
* func:定时器容器类的构造函数
* --初始化双向链表
*/
sort_timer_lst::sort_timer_lst()
{
head = NULL;
tail = NULL;
}


/*
* func:定时器容器类的析构函数
* --从双向链表头节点开始依次释放各个节点
*/
sort_timer_lst::~sort_timer_lst()
{
util_timer *tmp = head;
while (tmp)
{
head = tmp->next;
delete tmp;
tmp = head;
}
}


/*
* func: 添加定时器
* --定时器容器的双向链表添加定时器对象节点
*/
void sort_timer_lst::add_timer(util_timer *timer)
{
if(!timer)
{
// 若传入的定时器对象为NULL,直接return
return;
}
if(!head)
{
// 若头节点为空
// 新添加的定时器为双向链表的第一个定时器节点
head = tail = timer;
return;
}
// 双向链表中各个定时器中是按照expire(定时器到期时间)从小到大排序
// 若新定时器超时时间小于当前头部节点
// 直接将当前定时器结点作为头部结点
if(timer->expire < head->expire)
{
// 新插入节点的后驱指向头节点
timer->next = head;
// 头节点前驱指向新插入节点
head->prev = timer;
// 将头节点指针指向新插入节点
head = timer;
return;
}
// 非以上情况,就需要从头节点开始进行遍历双向链表
// 将新节点插入到合适的位置
add_timer(timer,head);
}


/*
* func:调整定时器,任务发生变化时,调整定时器在链表中的位置
* -- 比如说,一个连接在上一次到期时间内,突然又有了信息通信,就需要将该连接的定时器超时时间进行更新(增加)
*/
void sort_timer_lst::adjust_timer(util_timer *timer)
{
if(!timer)
{
// 若传入的定时器对象为NULL,直接return
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);
}
}


/*
* func:删除定时器:即是双向链表节点的删除
*/
void sort_timer_lst::adjust_timer(util_timer *timer)
{
if(!timer)
{
// 若传入的定时器对象为NULL,直接return
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);
}
}


/*
* func:add_timer(util_timer *timer)/adjust_timer函数中添加定时器节点的私有函数
* --主要用于调整链表内部结点
* --当定时器节点的插入需要遍历双向链表才能完成时,调用
*/
void sort_timer_lst::add_timer(util_timer *timer,util_timer *lst_head)
{
// 因为既然调用该函数,那么插入的定时器到期时间
// 一定比头节点位置的定时器到期时间长
util_timer *prev = lst_head;
// 因此开始阶段直接与头节点的后驱节点进行比较
util_timer *tmp = prev->next;

// 从双向链表中找到定时器应该放置的位置
// 即遍历一遍双向链表找到对应的位置
// 优化:此处的时间复杂度为O(n) 可以考虑C++11的优先队列进行优化--
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;
}
}


3.定时任务处理函数

使用统一事件源,SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器

具体逻辑,如下:

遍历定时器升序链表容器,从头结点开始依次处理每个定时器,直到遇到尚未到期的定时器

  • 因为链表是按照到期时间进行升序组织的,若当前定时器未到期,则后面的定时器更加没有到期

若当前时间小于定时器超时时间,跳出循环,即未找到到期的定时器

若当前时间大于定时器超时时间,即找到了到期的定时器,执行回调函数,然后将它从链表中删除,然后继续遍历

timer/lst_timer.cpp

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
/*
* func:定时器任务处理函数
* --使用统一事件源(alarm闹钟SIGALRM信号,通过管道发送给主循环,添加到epoll实例上,在管道读端进行监测)
* --SIGALRM信号每次被触发,主循环中调用一次定时任务处理函数,处理链表容器中到期的定时器
* 处理逻辑:
* step1.遍历定时器升序链表容器,从头结点开始依次处理每个定时器,直到遇到尚未到期的定时器
* --因为链表是按照到期时间进行升序组织的,若当前定时器未到期,则后面的定时器更加没有到期
* step2.若当前时间小于定时器超时时间,跳出循环,即未找到到期的定时器
* step3.若当前时间大于定时器超时时间,即找到了到期的定时器,执行回调函数,然后将它从链表中删除,然后继续遍历
*/
void sort_timer_lst::tick()
{
// 头节点为NULL,则链表为空
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;
}
}


4.如何使用定时器

服务器首先创建定时器容器链表,然后用统一事件源将异常事件,读写事件和信号事件统一处理,根据不同事件的对应逻辑使用定时器(下面代码是proactor模式下

具体的:

  • 监听到浏览器与服务器有新连接时,创建该连接对应的定时器,并将该定时器添加到链表上
  • 处理异常事件时,执行定时事件,服务器关闭连接,从链表上移除对应定时器
  • 处理定时信号时,将定时标志设置为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
142
143
144
145
146
147
148
// 创建定时器容器链表
static sort_timer_lst timer_lst;

// 创建连接资源数组
client_data *users_timer = new client_data[MAX_FD];

// 超时默认为False
bool timeout = false;

// alarm定时触发SIGALRM信号
alarm(TIMESLOT);

while (!stop_server)
{
// epoll_wait检测有就绪事件的文件描述符
int number = epoll_wait(epollfd, events, MAX_EVENT_NUMBER, -1);
if (number < 0 && errno != EINTR)
{
// error
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);
}

// 处理异常事件
// EPOLLRDHUP 远程连接已关闭
// EPOLLHUP 表示文件描述符被挂起,连接异常或者套接字连接被对方关闭
// EPOLLERR 套接字文件描述符发生错误
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);
}
}

// 处理定时器信号
// sockfd == pipefd[0]表示当前管道读端就绪
else if ((sockfd == pipefd[0]) && (events[i].events & EPOLLIN))
{
// 接收到SIGALRM信号,timeout设置为True
}

// 处理客户连接上接收到的数据
else if (events[i].events & EPOLLIN)
{
// 创建定时器临时变量,将该连接对应的定时器取出来
util_timer *timer = users_timer[sockfd].timer;
// user 是http连接对象数组
// 主线程进行IO操作,将请求报文读取至users[sockfd].m_read_buf中
if (users[sockfd].read_once())
{
// 若监测到读事件,将该事件放入请求队列
// 主线程将IO操作完成,将准备好的数据封装好,放入线程池的请求队列,由子线程竞争,解析
pool->append(users + sockfd);

// 若有数据传输,则将定时器往后延迟3个单位
// 对其在链表上的位置进行调整
if (timer)
{
time_t cur = time(NULL);
timer->expire = cur + 3 * TIMESLOT;
timer_lst.adjust_timer(timer);
}
}
else
{
// 主线程IO操作失败
// 服务器端关闭连接,移除对应的定时器
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())
{
// 若有数据传输,则将定时器往后延迟3个单位
// 并对新的定时器在链表上的位置进行调整
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来判断是否异地登录等等