在严肃的网络程序中,应用层的心跳协议是必不可少的。应该用心跳消息来判断对方进程是否能正常工作,“踢掉空闲连接”只是一时权宜之计。我这里想顺便讲讲 shared_ptr 和 weak_ptr 的用法。
如果一个连接连续几秒钟(后文以 8s 为例)内没有收到数据,就把它断开,为此有两种简单粗暴的做法:
使用 timing wheel 能避免上述两种做法的缺点。timing wheel 可以翻译为“时间轮盘”或“刻度盘”,本文保留英文。
连接超时不需要精确定时,只要大致 8 秒钟超时断开就行,多一秒少一秒关系不大。处理连接超时可以用一个简单的数据结构:8 个桶组成的循环队列。第一个桶放下一秒将要超时的连接,第二个放下 2 秒将要超时的连接。每个连接一收到数据就把自己放到第 8 个桶,然后在每秒钟的 callback 里把第一个桶里的连接断开,把这个空桶挪到队尾。这样大致可以做到 8 秒钟没有数据就超时断开连接。更重要的是,每次不用检查全部的 connection,只要检查第一个桶里的 connections,相当于把任务分散了
《Hashed and hierarchical timing wheels: efficient data structures for implementing a timer facility》这篇论文详细比较了实现定时器的各种数据结构,并提出了层次化的 timing wheel 与 hash timing wheel 等新结构。针对本文要解决的问题的特点,我们不需要实现一个通用的定时器,只用实现 simple timing wheel 即可。
Simple timing wheel 的基本结构是一个循环队列,还有一个指向队尾的指针 (tail),这个指针每秒钟移动一格,就像钟表上的时针,timing wheel 由此得名。
以下是某一时刻 timing wheel 的状态,格子里的数字是倒计时(与通常的 timing wheel 相反),表示这个格子(桶子)中的连接的剩余寿命。
一秒钟以后,tail 指针移动一格,原来四点钟方向的格子被清空,其中的连接已被断开。
假设在某个时刻,conn 1 到达,把它放到当前格子中,它的剩余寿命是 7 秒。此后 conn 1 上没有收到数据。
1 秒钟之后,tail 指向下一个格子,conn 1 的剩余寿命是 6 秒。
又过了几秒钟,tail 指向 conn 1 之前的那个格子,conn 1 即将被断开。
下一秒,tail 重新指向 conn 1 原来所在的格子,清空其中的数据,断开 conn 1 连接。
如果在断开 conn 1 之前收到数据,就把它移到当前的格子里。
收到数据,conn 1 的寿命延长为 7 秒。
时间继续前进,conn 1 寿命递减,不过它已经比第一种情况长寿了。
timing wheel 中的每个格子是个 hash set,可以容纳不止一个连接。
比如一开始,conn 1 到达。
随后,conn 2 到达,这时候 tail 还没有移动,两个连接位于同一个格子中,具有相同的剩余寿命。(下图中画成链表,代码中是哈希表。)
几秒钟之后,conn 1 收到数据,而 conn 2 一直没有收到数据,那么 conn 1 被移到当前的格子中。这时 conn 1 的寿命比 conn 2 长。