C++手写HTTP服务器二进制文件上传损坏问题求助
二进制文件上传损坏的修复方案
核心问题分析
你的服务器代码存在多个逻辑错误,直接导致二进制文件上传后损坏:
- TCP流式数据读取错误:两次独立
recv拆分HTTP头和文件数据,忽略了TCP的流式特性,会导致数据边界混乱——部分文件数据可能留在HTTP头缓冲区,或部分HTTP头混入文件数据缓冲区。 - 文件名解析指针偏移错误:用
strlen("filename ")(带空格)代替正确的strlen("filename=\""),导致文件名解析起始位置错误,进而影响文件内容的写入起点。 - 文件内容起始位置计算错误:
\r\n\r\n是4字节的HTTP头与内容分隔符,你错误的偏移计算导致写入文件时跳过部分有效数据或包含多余的HTTP头内容。 - 固定截断字节数错误:WebKit生成的FormData边界字符串长度不固定(每次上传都是随机值),固定截断45字节会导致要么多删有效数据,要么残留边界字符串,直接损坏二进制文件。
- Socket资源重复初始化:每次处理连接都重复创建、绑定、监听Socket,不仅效率低下,还可能引发资源泄漏。
- 缺乏错误检查:
fopen、recv、fwrite等操作未检查返回值,隐性错误无法及时发现和处理。
快速修复方案
1. 重构Socket初始化逻辑
将Socket创建、绑定、监听移到main函数,避免重复初始化:
#include <stdio.h> #include <string.h> #include <stdlib.h> #include <winsock2.h> #include <windows.h> #define PORT 12345 WSADATA ws; SOCKET soc, acpt; unsigned long long FileSize(char * direc){ unsigned long long size = 0; FILE *fsize = fopen(direc,"rb"); if(fsize == NULL) return 0; fseek(fsize,0,SEEK_END); size = _ftelli64(fsize); fseek(fsize,0,SEEK_SET); fclose(fsize); return size; } void Serve(){ int RecvBytes=0; sockaddr_in peerAddr; int peerAddrLen = sizeof(peerAddr); struct timeval RequestTimeout = {5,0} , DownloadTimeout= {0,500000}; char FileName[260]={0}; char boundary[256] = {0}; char fullBuffer[8192] = {0}; // 增大缓冲区容纳完整HTTP头和部分文件数据 int totalRecv = 0; acpt = accept(soc,(sockaddr*)&peerAddr, &peerAddrLen); if(acpt == INVALID_SOCKET){ printf("Accept failed\n"); return; } fd_set rfd; FD_ZERO(&rfd); FD_SET(acpt,&rfd); int connstat = select(acpt+1,&rfd,NULL,NULL,&RequestTimeout); if(!connstat){ closesocket(acpt); return; } // 读取完整HTTP头 while((RecvBytes = recv(acpt, fullBuffer + totalRecv, sizeof(fullBuffer) - totalRecv, 0)) > 0){ totalRecv += RecvBytes; // 检查是否已读取到HTTP头结束标记 if(strstr(fullBuffer, "\r\n\r\n") != NULL){ break; } if(totalRecv >= sizeof(fullBuffer)){ printf("Buffer overflow\n"); closesocket(acpt); return; } } if(RecvBytes <= 0){ closesocket(acpt); return; } // 提取boundary char *contentType = strstr(fullBuffer, "Content-Type: multipart/form-data; boundary="); if(contentType == NULL){ printf("Invalid multipart request\n"); closesocket(acpt); return; } strcpy(boundary, contentType + strlen("Content-Type: multipart/form-data; boundary=")); // 去掉boundary后的换行 char *newline = strstr(boundary, "\r\n"); if(newline) *newline = '\0'; // 构造结束边界字符串 char endBoundary[256]; snprintf(endBoundary, sizeof(endBoundary), "--%s--", boundary); // 解析文件名 char *namePtr = strstr(fullBuffer, "filename=\""); if(namePtr == NULL){ printf("No filename found\n"); closesocket(acpt); return; } namePtr += strlen("filename=\""); int x = 0; while(namePtr[x] != '"' && x < sizeof(FileName)-1){ x++; } strncpy(FileName, namePtr, x); printf("NAME: %s\n\n", FileName); // 找到文件内容起始位置 char *contentStart = strstr(fullBuffer, "\r\n\r\n"); if(contentStart == NULL){ printf("No content found\n"); closesocket(acpt); return; } contentStart += 4; // 跳过\r\n\r\n int contentLenInBuffer = totalRecv - (contentStart - fullBuffer); // 打开文件准备写入 FILE *f = fopen(FileName, "wb"); if(f == NULL){ printf("Failed to open file\n"); closesocket(acpt); return; } // 写入缓冲区中的文件内容 fwrite(contentStart, 1, contentLenInBuffer, f); // 继续读取剩余文件数据 char fileBuffer[2048]; while(1){ FD_ZERO(&rfd); FD_SET(acpt,&rfd); connstat = select(acpt+1,&rfd,NULL,NULL,&DownloadTimeout); if(!connstat){ break; } RecvBytes = recv(acpt, fileBuffer, sizeof(fileBuffer), 0); if(RecvBytes <= 0){ break; } // 检查是否包含结束边界 char *endPos = strstr(fileBuffer, endBoundary); if(endPos != NULL){ // 写入结束边界之前的内容 fwrite(fileBuffer, 1, endPos - fileBuffer, f); break; } fwrite(fileBuffer, 1, RecvBytes, f); } fclose(f); printf("OUTPUT FILE SIZE: %llu\n\n", FileSize(FileName)); MessageBeep(MB_ICONINFORMATION); shutdown(acpt, SD_BOTH); closesocket(acpt); acpt = NULL; memset(fullBuffer, '\0', sizeof(fullBuffer)); } int main(){ WSAStartup(MAKEWORD(2,2),&ws); int optval = 1; sockaddr_in addr; addr.sin_addr.s_addr = inet_addr("0.0.0.0"); addr.sin_family = AF_INET; addr.sin_port = htons(PORT); soc = socket(AF_INET,SOCK_STREAM,IPPROTO_TCP); setsockopt(soc,SOL_SOCKET,SO_REUSEADDR,(const char*)&optval,sizeof(optval)); bind(soc,(sockaddr*)&addr,sizeof(addr)); listen(soc,100); while(1){ Serve(); } closesocket(soc); WSACleanup(); return 0; }
2. HTML代码无需修改
你的AJAX上传逻辑正确,保持原样即可:
<!DOCTYPE html> <html> <body> The content of the body element is displayed in your browser. <input type="file" id="image-file"> <button onclick="Send()">Send</button> <script> function Send(){ let photo = document.getElementById("image-file").files[0]; let req = new XMLHttpRequest(); let formData = new FormData(); formData.append("photo", photo); req.open("POST", 'http://localhost:12345/'); req.send(formData); } </script> </body> </html>
修复后的关键改进
- 正确处理TCP流式数据,确保HTTP头和文件数据的边界划分准确
- 动态提取multipart boundary,避免固定截断导致的文件损坏
- 重构Socket初始化逻辑,提升效率并避免资源泄漏
- 添加完整的错误检查,便于排查隐性问题
- 准确计算文件内容的起始和结束位置,保证二进制数据完整写入
内容的提问来源于stack exchange,提问作者CoderBittu
相关产品推荐
相关产品推荐

