C++ UDP套接字传输HTML文件时字节数异常问题求助
UDP文件传输字节数不符问题排查与修复
问题概述
使用C++ UDP套接字实现客户端-服务器文件传输,传输1,814,247字节的s.html文件后出现两个异常:
- 客户端扣除8字节头部后的接收字节统计为1,814,239字节,比原文件少8字节;
- 生成的目标文件大小仅1,810,500字节,与原文件差距较大。
数据包结构体定义
struct ack_packet { uint16_t chsum; uint16_t len; uint16_t ackno; }; struct packet { uint16_t chsum; uint16_t len; uint16_t seqno; uint16_t finished; char data[500]; };
问题根源分析
1. 接收字节统计少8字节的原因
服务端发送最后一块数据包时,pkt.len被错误设置为lastChunkSize(仅数据长度),但实际数据包总长度应为8字节头部 + 数据长度。客户端通过s += numBytes -8统计有效数据,而服务端发送的数据包长度仅为lastChunkSize,导致numBytes-8比实际少8字节。
2. 目标文件大小远小于原文件的原因
- 最后一块数据包发送长度错误:服务端发送最后一块时,
sendto仅发送lastChunkSize字节,未发送完整的数据包头部+数据,导致客户端收到的packet结构体字段值混乱,receivedPacket.len无效。 - ACK接收逻辑错误:服务端接收最后一块的ACK时,仅尝试接收2字节,但
ack_packet实际是6字节,会导致ACK接收不完整;同时代码块未加花括号,recvfrom语句无论发送是否成功都会执行,干扰流程。 - 客户端写入依赖错误字段:客户端用
receivedPacket.len-8作为写入长度,但服务端错误设置len字段,导致写入长度远小于实际数据长度。 - 循环边界处理遗漏:当文件大小刚好是
chunkSize整数倍时,最后一块的finished标记逻辑存在疏漏,可能无法触发客户端的循环终止条件。
修复后的代码
服务端代码
const size_t chunkSize = 500; // 计算总数据包数:向上取整,处理整除与非整除情况 uint16_t totalPackets = (fileSize + chunkSize - 1) / chunkSize; for (uint16_t seqno = 0; seqno < totalPackets; ++seqno) { packet pkt{}; pkt.seqno = seqno; pkt.chsum = 88; // 建议后续替换为真实校验和逻辑 bool isLastPacket = (seqno == totalPackets - 1); pkt.finished = isLastPacket ? 1 : 0; // 计算当前块的数据长度 size_t currentChunkSize = isLastPacket ? (fileSize % chunkSize) : chunkSize; if (isLastPacket && currentChunkSize == 0) { currentChunkSize = chunkSize; // 处理文件大小刚好整除的情况 } pkt.len = currentChunkSize + 8; // 总长度=头部8字节+数据长度 memcpy(pkt.data, fileContent.data() + seqno * chunkSize, currentChunkSize); // 发送完整数据包 ssize_t b = sendto(newSock, &pkt, pkt.len, 0, &clntAddr, clntAddrLen); if (b < 0) { perror("sendto() failed"); seqno--; // 发送失败,重发当前包 continue; } // 接收完整的ACK包(6字节) ack_packet receivedPacket{}; socklen_t addrLen = clntAddrLen; ssize_t ackBytes = recvfrom(newSock, &receivedPacket, sizeof(receivedPacket), MSG_WAITALL, &clntAddr, &addrLen); if (ackBytes < 0 || receivedPacket.ackno != seqno) { seqno--; // ACK错误或接收失败,重发当前包 } }
客户端代码
int s = 0; struct sockaddr temp{}; socklen_t sz = sizeof(temp); uint16_t waiting_for_package_x = 0; // 以二进制模式打开文件,避免文本模式的换行符转换 ofstream outputFile("received_s.html", ios::binary); if (!outputFile.is_open()) { DieWithSystemMessage("Failed to open output file"); } while(true){ packet receivedPacket{}; ssize_t numBytes = recvfrom(sock, &receivedPacket, sizeof(receivedPacket), MSG_WAITALL, &temp, &sz); if (numBytes < 0) { DieWithSystemMessage("recvfrom() failed"); } // 验证数据包长度是否合法 if (numBytes != receivedPacket.len) { cout << "Invalid packet length: expected " << receivedPacket.len << ", got " << numBytes << endl; exit(-1); } if(receivedPacket.seqno != waiting_for_package_x){ cout << "Out of order packet: received " << receivedPacket.seqno << ", expected " << waiting_for_package_x << endl; exit(-1); } size_t dataLen = receivedPacket.len - 8; s += dataLen; outputFile.write(receivedPacket.data, dataLen); // 检查写入是否成功 if (!outputFile) { DieWithSystemMessage("File write failed"); } // 发送完整ACK ack_packet ackPacket{}; ackPacket.chsum = 0; // 建议后续替换为真实校验和逻辑 ackPacket.ackno = receivedPacket.seqno; ackPacket.len = sizeof(ackPacket); sendto(sock, &ackPacket, ackPacket.len, 0, &temp, sz); waiting_for_package_x++; if(receivedPacket.finished == 1){ break; } } // 刷新缓存并关闭文件,确保所有数据写入磁盘 outputFile.flush(); outputFile.close(); cout << "Total received data bytes (excluding headers): " << s << endl;
额外注意事项
- 二进制模式打开文件:必须添加
ios::binary,避免Windows下文本模式自动转换换行符导致文件大小变化。 - 实现真实校验和:当前硬编码的
chsum无法检测数据包损坏,建议用CRC32或简单异或校验增强可靠性。 - 添加超时重传:UDP是不可靠协议,当前仅在ACK不匹配时重发,需添加超时机制避免因ACK丢失导致死等。
- 传输前同步文件大小:服务端可先发送包含文件大小的控制包,客户端最终可校验接收文件的完整性。
内容的提问来源于stack exchange,提问作者Abdullah Taman
相关产品推荐
相关产品推荐

