cpp
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;
};
class sort_timer_lst
{
public:
sort_timer_lst();
~sort_timer_lst();
void add_timer(util_timer *timer);
void adjust_timer(util_timer *timer);
void del_timer(util_timer *timer);
void tick();
private:
void add_timer(util_timer *timer, util_timer *lst_head);
util_timer *head;
util_timer *tail;
};
从数据结构角度对这段代码进行更细致的拆解,结合其在网络编程中的典型应用场景(如TCP服务器的超时连接管理),分析每个结构的设计目的、交互逻辑及底层数据结构特性。
一、client_data
结构体:业务数据与定时器的"绑定器"
client_data
是网络连接信息与定时器的中间载体,其核心作用是建立"客户端连接"与"定时器"的双向映射,这是数据结构服务于业务逻辑的关键设计。
成员解析:
sockaddr_in address
:存储客户端的IP地址和端口号(网络编程中标准的地址结构),用于标识客户端的网络身份。int sockfd
:客户端的套接字描述符(文件描述符),是操作系统标识一个网络连接的唯一"句柄",通过它可以执行读写、关闭等操作。util_timer* timer
:指向该客户端对应的定时器。当客户端活动(如发送数据)时,需要通过这个指针找到对应的定时器并更新其过期时间;当定时器过期时,也能通过它反向关联到客户端数据。
设计意义:
在网络服务器中,每个客户端连接都需要一个定时器(用于检测"长时间无活动"的超时连接并关闭)。client_data
通过timer
指针将"连接"与"定时器"绑定,使得两者的状态可以相互影响,实现"活动重置定时器,超时关闭连接"的核心逻辑。
二、util_timer
类:双向链表节点的"多功能载体"
util_timer
是定时器链表的节点结构,它不仅是双向链表的组成单元,还封装了定时器的核心业务信息(过期时间、回调函数、关联的客户端数据)。
成员解析:
- 双向链表指针 :
prev
(前驱节点)和next
(后继节点)。这两个指针是双向链表的标志性特征,用于维护节点之间的逻辑关系:- 相比单向链表(只有
next
指针),双向链表的优势在于:删除节点时无需从头遍历查找前驱(已知节点时可直接通过prev
操作),时间复杂度为O(1);支持双向遍历(如从尾部向前查找插入位置)。
- 相比单向链表(只有
- 过期时间 :
time_t expire
(本质是一个整数,代表从某个时间点到过期时刻的秒数)。这是链表排序的唯一依据 ,链表中的节点按expire
从小到大(升序)排列。 - 回调函数 :
void (*cb_func)(client_data*)
。这是定时器过期时的"处理逻辑入口",通常指向关闭客户端连接、释放资源的函数。通过函数指针设计,使定时器节点与具体处理逻辑解耦(同一个链表可支持不同的回调逻辑)。 - 关联数据 :
client_data* user_data
。指向该定时器对应的客户端数据,使得回调函数被调用时,能通过这个指针找到要处理的客户端(如通过sockfd
关闭连接)。
节点特性:
每个util_timer
节点是"数据(expire
)+ 指针(prev
/next
)+ 业务关联(cb_func
/user_data
)"的综合体,既满足双向链表的结构需求,又承载了定时器的业务逻辑。
三、sort_timer_lst
类:有序双向链表的"管理中枢"
sort_timer_lst
是对双向链表的封装管理类 ,提供了定时器的添加、调整、删除和过期处理等核心操作。其核心设计目标是:维护链表的"按过期时间升序排列"特性,并高效处理定时器事件。
1. 成员变量:链表的"边界标识"
head
:指向链表的头节点(expire
最小的节点,最先可能过期)。tail
:指向链表的尾节点(expire
最大的节点,最后过期)。
这两个指针的作用是简化链表操作:例如添加节点时可从头部开始遍历,删除节点时无需判断是否为头/尾节点(通过prev
/next
直接操作)。
2. 核心方法解析:基于双向链表的操作逻辑
(1)add_timer(util_timer* timer)
:添加定时器(核心是"保持有序")
目标 :将新定时器插入链表,使插入后链表仍按expire
升序排列。
实现逻辑:
- 若链表为空(
head == NULL
),则新节点既是头也是尾。 - 若新节点
expire
小于头节点expire
,则插入到头节点前(成为新头)。 - 否则,调用私有方法
add_timer(timer, head)
,从head
开始遍历,找到第一个expire
大于新节点expire
的节点,将新节点插入到该节点的前面(通过调整prev
和next
指针完成双向链表的插入)。
示例 :
现有链表:timer1(expire=10) <-> timer2(expire=20) <-> timer3(expire=30)
插入timer4(expire=25)
,遍历找到timer3
(第一个expire>25
的节点),插入后:
timer1 <-> timer2 <-> timer4 <-> timer3
时间复杂度:O(n)(最坏情况需遍历整个链表,如插入到尾部)。
(2)adjust_timer(util_timer* timer)
:调整定时器位置(应对过期时间变化)
场景 :当客户端有新活动(如发送数据)时,需要延长其定时器的过期时间(如从expire=10
改为expire=30
),此时节点在链表中的位置需要重新调整。
实现逻辑:
- 若定时器是头节点,或新
expire
仍小于其后继节点的expire
:无需调整(仍保持有序)。 - 否则:先调用
del_timer(timer)
从链表中删除该节点,再调用add_timer(timer)
重新插入(按新expire
找到正确位置)。
本质:通过"删除+重新插入"实现位置调整,利用双向链表的动态性适应数据变化。
(3)del_timer(util_timer* timer)
:删除定时器(双向链表的节点移除)
实现逻辑:
- 若节点是头节点(
timer == head
):更新head
为timer->next
;若新head
不为空,需将新head->prev
置为NULL
(否则新头的前驱仍指向被删除节点)。 - 若节点是尾节点(
timer == tail
):更新tail
为timer->prev
;若新tail
不为空,需将新tail->next
置为NULL
。 - 若节点是中间节点:通过
timer->prev->next = timer->next
和timer->next->prev = timer->prev
,将节点从链表中"摘出"。
时间复杂度:O(1)(已知节点位置时,无需遍历)。
(4)tick()
:处理过期定时器(链表有序性的核心价值)
作用:定期(如每隔1秒)调用,检查并处理所有已过期的定时器。
实现逻辑:
- 若链表为空,直接返回。
- 循环判断头节点
head
是否过期(head->expire <= 当前时间
):- 若是:执行头节点的回调函数(
head->cb_func(head->user_data)
,如关闭客户端连接)。 - 保存当前头节点到临时变量
tmp
,更新head
为head->next
;若新head
不为空,将new head->prev
置为NULL
。 - 释放
tmp
节点的内存。
- 若是:执行头节点的回调函数(
- 直到头节点未过期(由于链表有序,后续节点
expire
均大于头节点,无需继续判断)。
示例 :
当前时间为25,链表:timer1(10) <-> timer2(20) <-> timer4(25) <-> timer3(30)
tick()
会依次处理timer1
(10≤25)、timer2
(20≤25)、timer4
(25≤25),最终链表仅剩timer3
。
时间复杂度:O(k)(k为过期节点数,通常远小于总节点数n),这是链表"有序性"带来的效率优势(无需遍历所有节点)。
四、数据结构设计的"权衡与适配"
这段代码选择有序双向链表作为定时器的底层数据结构,是对"实现复杂度"和"场景需求"的权衡:
优势:
- 实现简单:双向链表的节点插入、删除、遍历逻辑直观,代码量少,易于维护。
- 有序性适配定时器场景 :定时器的核心需求是"按时间顺序处理过期事件",有序链表的
tick()
操作可高效处理头部过期节点。 - 动态调整灵活 :通过
adjust_timer
的"删插"逻辑,可适应定时器过期时间的动态变化。
局限性:
- 插入效率低(O(n)):当定时器数量庞大(如10万级)时,插入操作需要遍历链表,性能瓶颈明显。
- 不适合高频更新场景 :若客户端活动频繁(需频繁调用
adjust_timer
),"删插"操作的累计开销会增大。
替代方案(更优数据结构):
- 时间轮(Time Wheel):将时间划分为固定刻度(如1秒/格),每个刻度对应一个链表,定时器按"过期时间÷刻度"映射到对应格子。插入和删除效率接近O(1),适合定时时间分散但更新频繁的场景(如服务器心跳检测)。
- 最小堆(Min-Heap) :用堆结构存储定时器,堆顶为
expire
最小的节点。插入和删除效率为O(log n),适合定时器数量多但更新不频繁的场景。
总结
这段代码是"数据结构服务于业务逻辑"的典型案例:
- 用
client_data
关联"网络连接"和"定时器",解决业务对象与数据结构的绑定问题; - 用
util_timer
实现"双向链表节点+定时器信息"的融合,满足结构和业务双重需求; - 用
sort_timer_lst
管理有序双向链表,通过维护expire
的升序性,实现高效的过期事件处理。
其设计简洁且适配中小规模的定时器场景(如轻量级服务器),但在高并发场景下需结合更高效的数据结构(如时间轮)优化性能。