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

C++手写HTTP服务器二进制文件上传损坏问题求助

二进制文件上传损坏的修复方案

核心问题分析

你的服务器代码存在多个逻辑错误,直接导致二进制文件上传后损坏:

  1. TCP流式数据读取错误:两次独立recv拆分HTTP头和文件数据,忽略了TCP的流式特性,会导致数据边界混乱——部分文件数据可能留在HTTP头缓冲区,或部分HTTP头混入文件数据缓冲区。
  2. 文件名解析指针偏移错误:用strlen("filename ")(带空格)代替正确的strlen("filename=\""),导致文件名解析起始位置错误,进而影响文件内容的写入起点。
  3. 文件内容起始位置计算错误:\r\n\r\n是4字节的HTTP头与内容分隔符,你错误的偏移计算导致写入文件时跳过部分有效数据或包含多余的HTTP头内容。
  4. 固定截断字节数错误:WebKit生成的FormData边界字符串长度不固定(每次上传都是随机值),固定截断45字节会导致要么多删有效数据,要么残留边界字符串,直接损坏二进制文件。
  5. Socket资源重复初始化:每次处理连接都重复创建、绑定、监听Socket,不仅效率低下,还可能引发资源泄漏。
  6. 缺乏错误检查: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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.07 11:54:55