C++ Linux轻量级WebServer(四)超时连接
介绍
每个客户端都会设置一个超时时间,当到达超时时间时服务器会自动与客户端断开连接,以节省服务器的资源并提高服务器的性能如无需占用文件描述符fd
,不用在HttpTimer
对象中的heap_
容器中添加新的节点并管理它,也不用使用Epoll
去管理该客户端如检测EPOLLIN
与EPOLLOUT
事件等,在降低内存使用的同时,也加快了搜索的速度。
实现过程
该系统设定的超时时间为60000ms即60s,在每一轮使用epoll_wait(timeMS)
检测事件之前,会调用HttpTimer
对象即定时器的GetNextTick()
去清除超时节点,并获取最先超时连接节点的超时时间timeMS
,并将其作为epoll_wait()
的参数,当timeMS
时间内有事件发生时,让epoll_wait()
返回,否则阻塞到timeMS
后返回,而这样做的目的是减少epoll_wait()
调用次数,以提高效率。
注意:timeMS
初始设为-1,无事件epoll_wait()
将处于阻塞状态。
业务代码:
// 如果设置了超时时间,例如60s,则只要一个连接60秒没有读写操作,则关闭
if(timeoutMS_ > 0) {
// 通过定时器GetNextTick(),清除超时的节点,然后获取最先要超时的连接的超时的时间
timeMS = timer_->GetNextTick();
}
// timeMS是最先要超时的连接的超时的时间,传递到epoll_wait()函数中
// 当timeMS时间内有事件发生,epoll_wait()返回,否则等到了timeMS时间后才返回
// 这样做的目的是为了让epoll_wait()调用次数变少,提高效率
int eventCnt = epoller_->Wait(timeMS);
定时器又是如何实现的呢?
定时器是基于小根堆实现的,而小根堆并不是调用标准库而是由自己实现的,小根堆实现的两个核心函数是上调siftup_()
和下调siftdown_()
,上调指的是和父节点的超时时间相比,如果比父节点的超时时间小,则交换节点,直至超时时间比父节点大,同理,下调指的是和左右子节点超时时间较小的相比,如果比左右子节点超时时间较小的要大,则交换节点,直至该节点比左右子节点的超时时间都要小,而具体代码实现如下:
siftup_
函数:
void HeapTimer::siftup_(size_t i) {
assert(i >= 0 && i < heap_.size());
size_t j = (i - 1) / 2;
while(j >= 0) {
if(heap_[j] < heap_[i]) { break; }
SwapNode_(i, j);
i = j;
j = (i - 1) / 2;
}
}
siftdown_
函数:
bool HeapTimer::siftdown_(size_t index, size_t n) {
assert(index >= 0 && index < heap_.size());
assert(n >= 0 && n <= heap_.size());
size_t i = index;
size_t j = i * 2 + 1;
while(j < n) {
if(j + 1 < n && heap_[j + 1] < heap_[j]) j++;
if(heap_[i] < heap_[j]) break;
SwapNode_(i, j);
i = j;
j = i * 2 + 1;
}
return i > index;
}
交换节点SwapNode_()
函数,只是节点值之间的交换?,显然不是,是需要定义一个哈希表即unordered_map<int, size_t> ref_
,用于存储文件描述符与节点编号之间的映射关系,由此就可以通过文件描述符fd
定位到堆中的节点,再对堆中的节点操作,所以SwapNode()
不仅需要交换节点的值,还需要改变它们之间的映射关系。
SwapNode
函数:
void HeapTimer::SwapNode_(size_t i, size_t j) {
assert(i >= 0 && i < heap_.size());
assert(j >= 0 && j < heap_.size());
std::swap(heap_[i], heap_[j]);
ref_[heap_[i].id] = i;
ref_[heap_[j].id] = j;
}
在处理DealListen_()
函数时,会调用HttpTimer
对象timer_
的add()
函数,在add
中会建立文件描述符fd
和节点编号之间的映射关系,当然如果节点存在则直接调整堆即可。
add
函数:
void HeapTimer::add(int id, int timeout, const TimeoutCallBack& cb) {
assert(id >= 0);
size_t i;
if(ref_.count(id) == 0) {
/* 新节点:堆尾插入,调整堆 */
i = heap_.size(); // 节点编号
ref_[id] = i; // 文件描述符和节点编号之间映射关系 id - i(key - value)
heap_.push_back({id, Clock::now() + MS(timeout), cb});
siftup_(i); // 向上调整,跟父亲比较
}
else {
/* 已有结点:调整堆 */
i = ref_[id];
heap_[i].expires = Clock::now() + MS(timeout);
heap_[i].cb = cb;
if(!siftdown_(i, heap_.size())) {
siftup_(i);
}
}
}
而借助于上调siftup_
、下调siftdown_
与交换节点SwapNode_
,可以实现堆的插入、删除、调整指定堆节点的操作。
操作 | 具体步骤 | 时间复杂度 |
---|---|---|
添加 | 节点插入至heap_ 末尾,再向上调整即siftup_ | log(n) |
删除 | 将删除的节点换至heap_ 末尾,再向下或向上调整被换节点,删除heap_ 末尾节点 | log(n) |
调整堆指定节点 | 改变堆中节点值,再向下或向上调整 | log(n) |
对比于使用STL标准库而言,自定义实现的堆的操作更加灵活化与可定制,如STL无法实现删除堆中任意节点而只能实现删除堆顶节点,上述表格操作就不贴代码了,可自行查看heaptimer.cpp
的内容。
在与客户端建立连接时,会创建节点插入堆中,设置定时时间为60s,并绑定回调函数CloseConn_
去关闭客户端的连接,除了开头提到的清除超时节点外,在触发读事件与写事件时也会将该客户端的超时时长延长60s,因为在触发该客户端的读写事件时证明该客户端是处于活跃状态的。
结尾
目前为止,基于小根堆实现的定时器,关闭超时的非活动连接也解析完毕了,这里涉及了数据结构堆的知识,所以在面试过程中也是经常问到的,是需要完全掌握的。下章则会继续解析日志系统的实现。
转载自:https://juejin.cn/post/7107831560769896461