一个双向链表实例解析

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的节点,将新节点插入到该节点的前面(通过调整prevnext指针完成双向链表的插入)。

示例

现有链表: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):更新headtimer->next;若新head不为空,需将新head->prev置为NULL(否则新头的前驱仍指向被删除节点)。
  • 若节点是尾节点(timer == tail):更新tailtimer->prev;若新tail不为空,需将新tail->next置为NULL
  • 若节点是中间节点:通过timer->prev->next = timer->nexttimer->next->prev = timer->prev,将节点从链表中"摘出"。

时间复杂度:O(1)(已知节点位置时,无需遍历)。

(4)tick():处理过期定时器(链表有序性的核心价值)

作用:定期(如每隔1秒)调用,检查并处理所有已过期的定时器。

实现逻辑

  • 若链表为空,直接返回。
  • 循环判断头节点head是否过期(head->expire <= 当前时间):
    • 若是:执行头节点的回调函数(head->cb_func(head->user_data),如关闭客户端连接)。
    • 保存当前头节点到临时变量tmp,更新headhead->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),这是链表"有序性"带来的效率优势(无需遍历所有节点)。

四、数据结构设计的"权衡与适配"

这段代码选择有序双向链表作为定时器的底层数据结构,是对"实现复杂度"和"场景需求"的权衡:

优势:
  1. 实现简单:双向链表的节点插入、删除、遍历逻辑直观,代码量少,易于维护。
  2. 有序性适配定时器场景 :定时器的核心需求是"按时间顺序处理过期事件",有序链表的tick()操作可高效处理头部过期节点。
  3. 动态调整灵活 :通过adjust_timer的"删插"逻辑,可适应定时器过期时间的动态变化。
局限性:
  1. 插入效率低(O(n)):当定时器数量庞大(如10万级)时,插入操作需要遍历链表,性能瓶颈明显。
  2. 不适合高频更新场景 :若客户端活动频繁(需频繁调用adjust_timer),"删插"操作的累计开销会增大。
替代方案(更优数据结构):
  • 时间轮(Time Wheel):将时间划分为固定刻度(如1秒/格),每个刻度对应一个链表,定时器按"过期时间÷刻度"映射到对应格子。插入和删除效率接近O(1),适合定时时间分散但更新频繁的场景(如服务器心跳检测)。
  • 最小堆(Min-Heap) :用堆结构存储定时器,堆顶为expire最小的节点。插入和删除效率为O(log n),适合定时器数量多但更新不频繁的场景。

总结

这段代码是"数据结构服务于业务逻辑"的典型案例:

  • client_data关联"网络连接"和"定时器",解决业务对象与数据结构的绑定问题;
  • util_timer实现"双向链表节点+定时器信息"的融合,满足结构和业务双重需求;
  • sort_timer_lst管理有序双向链表,通过维护expire的升序性,实现高效的过期事件处理。

其设计简洁且适配中小规模的定时器场景(如轻量级服务器),但在高并发场景下需结合更高效的数据结构(如时间轮)优化性能。