算机在工作时,我们打开多个网页,但是不使用的时候,系统自动会进入休眠模式,这样会更加省电,节省资源。同样的, 服务器在工作时,建立好连接后,即使你不用,他也会一直为你服务吗?当然不是,不然可太消耗资源了。
对于非活跃的用户,达到了设定的时间,那我们就关闭这个连接,等用户再次需要时再重新建立连接。
怎么实现?
通常有以下几个必要步骤:
连接管理:服务器会维护一个连接池,其中包含当前所有已建立的连接。
设置超时时间:每当服务器接受一个新的连接或者收到一个请求时,会为该连接设置一个超时时间。这个超时时间通常是从当前时间开始计算的,用于确定连接在多长时间内没有活动将会被认为是非活跃连接。
定时器管理:服务器会维护一个小根堆(也称为最小堆),其中存储了所有连接的超时时间。这个小根堆会根据连接的超时时间进行组织,保证堆顶元素是最小的超时时间。
定时器触发:服务器会周期性地检查小根堆的堆顶元素,如果发现堆顶元素所代表的连接已经超时(即连接变为非活跃状态),则会关闭该连接并从连接池中移除。
调整定时器:如果有新的连接建立或者有活动的连接被检测到,服务器会更新小根堆中相应连接的超时时间,以确保定时器仍然能够准确地反映连接的活跃状态。
采用小根堆实现定时器有以下好处:
高效性:小根堆的插入、删除和查找最小值的时间复杂度都是 O(log n),使得定时器管理操作能够在较短的时间内完成。
实时性:定时器的触发是基于连接的超时时间,因此能够及时发现并关闭非活跃连接,释放服务器资源,提高服务器的响应速度和并发能力。
灵活性:可以根据需要动态调整超时时间,从而适应不同场景下连接的活跃性变化,提高了系统的适应性和灵活性。
采用小根堆实现定时器能够有效地管理和关闭非活跃连接,提高服务器的性能和稳定性。
代码部分
siftup_ 函数用于在向堆中插入新节点后,调整堆,使其仍然保持堆的性质(通常用于最小堆)。它将新插入的节点与其父节点进行比较,如果新节点的值小于父节点的值,则交换它们的位置,直到找到合适的位置或者达到堆顶。
void HeapTimer::siftup_(size_t i) { assert(i >= 0 && i < heap_.size()); // 确保索引 i 的合法性 size_t j = (i - 1) / 2; // 计算父节点索引 while(j >= 0) { if(heap_[j] < heap_[i]) { break; } // 如果父节点的超时时间小于当前节点的超时时间,则退出循环 SwapNode_(i, j); // 交换当前节点与父节点的位置 i = j; // 更新当前节点索引为父节点索引 j = (i - 1) / 2; // 更新父节点索引为新的父节点索引 } }
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]); // 交换堆中索引为 i 和 j 的两个节点 ref_[heap_[i].id] = i; // 更新交换后节点的索引映射 ref_[heap_[j].id] = j; }
siftdown_
函数用于在删除堆顶元素或者更新堆顶元素后,调整堆,使其仍然保持堆的性质。它将堆顶元素与其子节点进行比较,如果堆顶元素的值大于子节点的值,则将它们交换位置,直到找到合适的位置或者达到堆底。
bool HeapTimer::siftdown_(size_t index, size_t n) { assert(index >= 0 && index < heap_.size()); // 确保索引 index 的合法性 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; // 返回是否发生了节点的移动 }
add
函数用于向堆中添加新节点或者更新已有节点的超时时间和回调函数。如果添加的是新节点,则将其插入到堆的末尾并调用siftup_
函数;如果更新的是已有节点,则根据新的超时时间和当前堆中的情况,选择调用siftdown_
或者siftup_
函数来调整堆。
void HeapTimer::add(int id, int timeout, const TimeoutCallBack& cb) { assert(id >= 0); // 确保 id 非负 size_t i; if(ref_.count(id) == 0) { /* 新节点:堆尾插入,调整堆 */ i = heap_.size(); // 新节点的索引为堆的大小 ref_[id] = i; // 更新索引映射 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); // 如果向下调整失败,则向上调整堆 } } }
void HeapTimer::doWork(int id)
: 执行指定id对应的定时任务,即触发回调函数。如果堆为空或者指定id不存在,则直接返回。
void HeapTimer::doWork(int id) { /* 删除指定id结点,并触发回调函数 */ if(heap_.empty() || ref_.count(id) == 0) { return; // 如果堆为空或者指定id不存在,则直接返回 } size_t i = ref_[id]; // 获取指定id在堆中的索引 TimerNode node = heap_[i]; // 获取指定id对应的定时器节点 node.cb(); // 执行该节点的回调函数 del_(i); // 删除该节点 }
void HeapTimer::del_(size_t index)
: 删除指定位置的定时任务节点。首先检查堆是否为空,并确保索引合法,然后将要删除的节点与队尾节点交换位置,并调整堆使其保持堆的性质,最后删除队尾节点。
void HeapTimer::del_(size_t index) { /* 删除指定位置的结点 */ assert(!heap_.empty() && index >= 0 && index < heap_.size()); // 断言堆不为空且索引合法 size_t i = index; // 待删除节点的索引 size_t n = heap_.size() - 1; // 堆的最后一个节点索引 assert(i <= n); // 断言待删除节点索引不超过堆的最后一个节点索引 if(i < n) { SwapNode_(i, n); // 将待删除节点与堆的最后一个节点交换位置 if(!siftdown_(i, n)) { // 尝试向下调整堆 siftup_(i); // 如果向下调整失败,则向上调整堆 } } /* 队尾元素删除 */ ref_.erase(heap_.back().id); // 从索引表中删除队尾节点对应的id heap_.pop_back(); // 删除队尾节点 }
void HeapTimer::adjust(int id, int timeout)
: 调整指定id对应的定时任务的超时时间。首先确保堆不为空且指定id存在,然后更新对应节点的超时时间,并调整堆以维持堆的性质。
void HeapTimer::adjust(int id, int timeout) { /* 调整指定id的结点的超时时间 */ assert(!heap_.empty() && ref_.count(id) > 0); // 断言堆不为空且指定id存在于索引表中 heap_[ref_[id]].expires = Clock::now() + MS(timeout); // 更新指定id对应节点的超时时间 siftdown_(ref_[id], heap_.size()); // 调整堆以保持堆的性质 }
void HeapTimer::tick()
: 检查并清除超时的定时任务节点。如果堆为空,则直接返回;否则,循环处理堆中的定时任务节点,直到堆为空或者堆顶节点未超时。在每次循环中,获取堆顶节点,检查其超时时间是否已到,如果到了则执行其回调函数并删除该节点。
void HeapTimer::tick() { /* 清除超时结点 */ if(heap_.empty()) { return; // 如果堆为空,则直接返回 } while(!heap_.empty()) { TimerNode node = heap_.front(); // 获取堆顶节点 if(std::chrono::duration_cast<MS>(node.expires - Clock::now()).count() > 0) { break; // 如果堆顶节点未超时,则退出循环 } node.cb(); // 执行堆顶节点的回调函数 pop(); // 弹出堆顶节点 } }
void HeapTimer::pop()
: 弹出堆顶的定时任务节点,即删除堆顶节点并重新调整堆。
void HeapTimer::pop() { assert(!heap_.empty()); // 堆不为空 del_(0); // 删除堆顶节点 }
void HeapTimer::clear()
: 清空堆定时器,即清空索引表和堆中的所有节点。
void HeapTimer::clear() { ref_.clear(); // 清空索引表 heap_.clear(); // 清空堆 }
int HeapTimer::GetNextTick()
: 获取下一个超时时间。首先调用tick()函数处理可能已超时的任务,然后检查堆是否为空,如果不为空则计算堆顶节点的剩余超时时间并返回,如果堆为空则返回-1表示无下一个超时任务。
int HeapTimer::GetNextTick() { tick(); // 先处理可能已超时的任务 size_t res = -1; if(!heap_.empty()) { res = std::chrono::duration_cast<MS>(heap_.front().expires - Clock::now()).count(); // 计算堆顶节点的剩余超时时间 if(res < 0) { res = 0; } // 如果剩余超时时间小于0,则设置为0 } return res; // 返回下一个超时时间 }