You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在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

火山引擎 最新活动