基于WinSockets的C语言实现Selective Repeat ARQ:ACK接收问题求助
解决WinSockets实现Selective Repeat ARQ的发送端ACK接收问题
我之前做过类似的WinSockets + Selective Repeat ARQ的实现,刚好能解决你的困惑。核心问题在于你不能用阻塞式的recvfrom()死等ACK——那样必然会退化成停止等待模式。你需要用多路复用机制来同时监听ACK的到来,并且在窗口有剩余空间时继续发送新数据包,WinSockets里有两种非常实用的方案:
方案1:用select()实现超时监听与并发处理
select()是跨平台的多路复用接口,在WinSockets里也完全适用。它可以让你在一段超时时间内,同时检查套接字是否有可读数据(也就是ACK到来),超时后还能处理重传逻辑。具体流程如下:
- 维护发送窗口状态:你需要一个结构体数组或者链表,记录窗口内每个数据包的状态:序号、是否已发送、是否已被ACK、发送时间戳(用于超时重传)。同时要跟踪窗口的起始(最小未ACK序号)和结束(最大已发送序号+1)位置。
- 初始化窗口:先把发送窗口填满,一次性发送窗口内的所有数据包。
- 进入循环处理:用
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
相关产品推荐
相关产品推荐

