如何在C语言中实现单线程多序号定时器用于通信协议重传?
当然可以不用多线程搞定!这种每个数据包对应独立定时器的需求,在单线程环境下完全能实现——核心就是用一个全局的定时器管理结构来跟踪每个seqn的超时时间,然后在主循环里定期轮询检查状态就行。下面给你详细的实现思路和伪代码:
不用多线程的关键是避免为每个定时器创建独立执行流,转而采用「轮询检查+时间戳记录」的方式:
- 为每个带seqn的数据包维护一个超时时间戳(记录它应该触发超时的绝对时间)
- 在主程序的事件循环里(比如和网络IO处理绑定),定期遍历所有未确认的定时器,判断是否超时
1. 定义定时器数据结构
首先我们需要一个结构来保存每个seqn的定时器状态,考虑到seqn通常是有范围的(比如滑动窗口内的序号),用数组存储是最高效的选择:
#define MAX_SEQN 1024 // 根据你的协议滑动窗口大小定义,可调整 // 定时器的两种状态 typedef enum { TIMER_IDLE, // 未启动/已结束 TIMER_RUNNING // 正在运行中 } TimerState; // 每个seqn对应的定时器信息 typedef struct { TimerState state; unsigned long long expire_time; // 超时的绝对时间(毫秒级时间戳) } PacketTimer; // 全局定时器数组,seqn直接作为索引快速访问 PacketTimer timers[MAX_SEQN];
如果你的seqn是无界的,也可以换成链表或者哈希表来存储,不过数组的访问效率是最高的。
2. 实现start_timer(seqn)函数
这个函数的作用是记录当前时间,加上预设的超时时长,得到该seqn的超时时间戳,同时标记定时器为运行状态:
// 工具函数:获取当前系统的毫秒级单调时间戳(不受系统时间调整影响) unsigned long long get_current_ms() { struct timespec ts; clock_gettime(CLOCK_MONOTONIC, &ts); return ts.tv_sec * 1000 + ts.tv_nsec / 1000000; } // 启动指定seqn的定时器,timeout_ms为超时时间(比如200ms) void start_timer(int seqn, int timeout_ms) { if (seqn < 0 || seqn >= MAX_SEQN) { // 处理非法seqn,比如打印日志或返回错误 return; } timers[seqn].state = TIMER_RUNNING; // 计算超时时间:当前时间 + 超时时长 timers[seqn].expire_time = get_current_ms() + timeout_ms; }
这里用CLOCK_MONOTONIC而不是系统时间,是因为它不会被系统时间的调整(比如手动改时间、NTP同步)影响,更适合计算时间间隔。
3. 实现timer_expired(seqn)函数
这个函数负责检查指定seqn的定时器是否处于运行状态,并且当前时间已经超过了预设的超时时间戳:
// 判断指定seqn的定时器是否超时,返回1表示超时,0表示未超时 int timer_expired(int seqn) { if (seqn < 0 || seqn >= MAX_SEQN) { return 0; } // 定时器未运行,直接返回未超时 if (timers[seqn].state != TIMER_RUNNING) { return 0; } // 对比当前时间和超时时间戳 return get_current_ms() >= timers[seqn].expire_time; }
4. 主循环中的定时器检查逻辑
在你的主程序循环里,需要把定时器检查和网络IO处理放在一起,形成一个事件驱动的循环:
int main() { // 初始化定时器数组为IDLE状态 memset(timers, 0, sizeof(timers)); while (1) { // 第一步:处理网络IO(接收ACK、接收数据包、发送新包等) handle_network_events(); // 第二步:遍历所有未确认的seqn,检查定时器 for (int seqn = 0; seqn < MAX_SEQN; seqn++) { if (!ack_received(seqn) && timer_expired(seqn)) { // 超时未收到ACK,重传数据包 send_packet(seqn); // 重启该seqn的定时器,可考虑用退避算法(比如超时时间翻倍) start_timer(seqn, 200); } } // 第三步:加一个小延迟,避免CPU占用过高(可选) usleep(1000); // 休眠1ms } }
另外,当你收到某个seqn的ACK时,一定要记得把对应的定时器状态重置为TIMER_IDLE,避免后续误判:
// 收到ACK后的处理函数 void on_ack_received(int seqn) { if (seqn >= 0 && seqn < MAX_SEQN) { timers[seqn].state = TIMER_IDLE; } // 其他ACK相关处理逻辑... }
如果你的滑动窗口很大,遍历整个数组会有点低效,可以维护一个**「活跃定时器链表」**,只把处于TIMER_RUNNING状态的seqn加入链表,并且按超时时间戳排序。这样每次检查时,只需要遍历链表,甚至可以提前退出(因为链表是按超时时间升序排列的,第一个未超时的节点后面的都不会超时):
// 链表节点结构 typedef struct TimerNode { int seqn; unsigned long long expire_time; struct TimerNode* next; } TimerNode; TimerNode* timer_head = NULL; // 启动定时器时插入链表(按超时时间升序排列) void start_timer(int seqn, int timeout_ms) { // 先做之前的状态更新 if (seqn < 0 || seqn >= MAX_SEQN) return; timers[seqn].state = TIMER_RUNNING; unsigned long long expire = get_current_ms() + timeout_ms; timers[seqn].expire_time = expire; // 创建新节点并插入链表的合适位置 TimerNode* new_node = malloc(sizeof(TimerNode)); new_node->seqn = seqn; new_node->expire_time = expire; new_node->next = NULL; // 插入到有序链表中 if (timer_head == NULL || expire < timer_head->expire_time) { new_node->next = timer_head; timer_head = new_node; } else { TimerNode* curr = timer_head; while (curr->next != NULL && curr->next->expire_time <= expire) { curr = curr->next; } new_node->next = curr->next; curr->next = new_node; } } // 检查定时器的逻辑改为遍历链表 void check_timers() { unsigned long long now = get_current_ms(); TimerNode** curr_ptr = &timer_head; while (*curr_ptr != NULL) { TimerNode* curr = *curr_ptr; // 如果已收到ACK,移除节点 if (ack_received(curr->seqn)) { *curr_ptr = curr->next; free(curr); timers[curr->seqn].state = TIMER_IDLE; } // 如果超时,重传并重启定时器 else if (now >= curr->expire_time) { send_packet(curr->seqn); start_timer(curr->seqn, 200); // 移除旧节点,因为start_timer会创建新节点插入链表 *curr_ptr = curr->next; free(curr); } // 链表是升序的,后面的节点都不会超时,直接退出 else { break; } } }
这样可以大大减少遍历的次数,提升程序效率。
内容的提问来源于stack exchange,提问作者Robb1




