You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

基于WinSockets的C语言实现Selective Repeat ARQ:ACK接收问题求助

解决WinSockets实现Selective Repeat ARQ的发送端ACK接收问题

我之前做过类似的WinSockets + Selective Repeat ARQ的实现,刚好能解决你的困惑。核心问题在于你不能用阻塞式的recvfrom()死等ACK——那样必然会退化成停止等待模式。你需要用多路复用机制来同时监听ACK的到来,并且在窗口有剩余空间时继续发送新数据包,WinSockets里有两种非常实用的方案:

方案1:用select()实现超时监听与并发处理

select()是跨平台的多路复用接口,在WinSockets里也完全适用。它可以让你在一段超时时间内,同时检查套接字是否有可读数据(也就是ACK到来),超时后还能处理重传逻辑。具体流程如下:

  1. 维护发送窗口状态:你需要一个结构体数组或者链表,记录窗口内每个数据包的状态:序号、是否已发送、是否已被ACK、发送时间戳(用于超时重传)。同时要跟踪窗口的起始(最小未ACK序号)和结束(最大已发送序号+1)位置。
  2. 初始化窗口:先把发送窗口填满,一次性发送窗口内的所有数据包。
  3. 进入循环处理:用select()监听套接字的读就绪状态,同时在每次循环中检查是否有新的数据包可以发送(窗口未被填满)。

代码片段示例

#include <winsock2.h>
#include <stdio.h>

#define WINDOW_SIZE 4
#define ACK_SIZE 4 // 假设ACK只包含4字节的序号
#define TIMEOUT_SEC 1

// 数据包状态结构体
typedef struct {
    int seq_num;
    char data[1024];
    int data_len;
    int is_sent;
    int is_acked;
    DWORD send_time; // 发送时间戳,用于超时判断
} Packet;

Packet send_window[WINDOW_SIZE];
int base = 0; // 窗口起始序号
int next_seq = 0; // 下一个要发送的序号

void update_send_window(int ack_seq) {
    // 标记对应序号的数据包为已ACK
    for (int i = 0; i < WINDOW_SIZE; i++) {
        if (send_window[i].seq_num == ack_seq) {
            send_window[i].is_acked = 1;
            break;
        }
    }
    // 滑动窗口:把base移到第一个未ACK的序号
    while (send_window[base % WINDOW_SIZE].is_acked) {
        base++;
    }
}

void send_new_packets(SOCKET sock, struct sockaddr_in dest_addr) {
    // 如果窗口还有空间,发送新的数据包
    while ((next_seq - base) < WINDOW_SIZE) {
        int idx = next_seq % WINDOW_SIZE;
        send_window[idx].seq_num = next_seq;
        send_window[idx].is_sent = 1;
        send_window[idx].is_acked = 0;
        send_window[idx].send_time = GetTickCount();

        // 发送数据包
        sendto(sock, send_window[idx].data, send_window[idx].data_len, 0, 
               (struct sockaddr*)&dest_addr, sizeof(dest_addr));
        
        next_seq++;
    }
}

void retransmit_unacked_packets(SOCKET sock, struct sockaddr_in dest_addr) {
    DWORD now = GetTickCount();
    // 重传所有超时未ACK的数据包
    for (int i = base; i < next_seq; i++) {
        int idx = i % WINDOW_SIZE;
        if (!send_window[idx].is_acked && (now - send_window[idx].send_time) > TIMEOUT_SEC * 1000) {
            printf("Retransmitting packet %d\n", send_window[idx].seq_num);
            sendto(sock, send_window[idx].data, send_window[idx].data_len, 0, 
                   (struct sockaddr*)&dest_addr, sizeof(dest_addr));
            send_window[idx].send_time = now; // 更新发送时间
        }
    }
}

int main() {
    // 初始化WinSockets的代码省略...
    SOCKET sock = socket(AF_INET, SOCK_DGRAM, 0);
    struct sockaddr_in dest_addr;
    // 初始化dest_addr的代码省略...

    // 先填满发送窗口
    send_new_packets(sock, dest_addr);

    fd_set read_fds;
    struct timeval tv;
    int ret;

    while (base < total_packets) { // total_packets是你要发送的总数据包数
        FD_ZERO(&read_fds);
        FD_SET(sock, &read_fds);

        // 设置超时时间
        tv.tv_sec = TIMEOUT_SEC;
        tv.tv_usec = 0;

        ret = select(0, &read_fds, NULL, NULL, &tv);
        if (ret > 0) {
            // 有ACK到来,接收并处理
            char ack_buf[ACK_SIZE];
            struct sockaddr_in recv_addr;
            int addr_len = sizeof(recv_addr);
            int bytes_read = recvfrom(sock, ack_buf, ACK_SIZE, 0, (struct sockaddr*)&recv_addr, &addr_len);
            if (bytes_read == ACK_SIZE) {
                int ack_seq = *(int*)ack_buf;
                printf("Received ACK for %d\n", ack_seq);
                update_send_window(ack_seq);
                // 窗口滑动后,尝试发送新的数据包
                send_new_packets(sock, dest_addr);
            }
        } else if (ret == 0) {
            // 超时,处理重传
            retransmit_unacked_packets(sock, dest_addr);
        } else {
            // select出错,处理错误
            printf("Select failed: %d\n", WSAGetLastError());
            break;
        }
    }

    // 清理资源的代码省略...
    return 0;
}

方案2:用WSAEventSelect()实现事件驱动

如果你更习惯Windows的事件驱动模型,可以用WSAEventSelect()把套接字的FD_READ事件(对应有数据可读,也就是ACK到来)绑定到一个事件对象上,然后用WaitForMultipleObjects()同时等待这个事件和一个超时事件,这样也能实现非阻塞的ACK接收和数据包发送。

这种方式的优势是更贴合Windows平台的异步模型,避免了select()的一些性能限制(比如文件描述符数量限制),但代码量会稍大一点。核心思路是:

  • 创建一个事件对象,关联套接字的FD_READ事件
  • 在循环中用WaitForMultipleObjects()等待事件触发或超时
  • 当FD_READ事件触发时,接收并处理ACK;超时则处理重传;同时在每次循环间隙检查窗口状态,发送新数据包

关键注意事项

  • 每个数据包单独计时:Selective Repeat需要为每个未ACK的数据包维护独立的超时计时器,而不是整个窗口共用一个超时——这样才能精准重传单个丢失的数据包,而不是整个窗口。
  • 匹配接收端窗口:发送窗口的大小不能超过接收端通告的接收窗口大小,否则会导致接收端无法处理,你需要在握手阶段或者数据包中交换窗口大小信息。
  • 序号循环处理:当序号达到最大值时,要用模运算实现循环序号(比如模2^n,n是你选择的序号位数),避免序号溢出导致的错误。

内容的提问来源于stack exchange,提问作者fugasjunior

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.27 10:08:51