【自用】解析http post表单数据,将其中的二进制数据保存到csv文件且加载到内存

解析http post表单数据,将其中的二进制数据保存到csv文件且加载到std::map<int, std::map<int, std::vector<std::pair<double, double>>>> calibration_map;内存

前端通过http post请求发过来的数据包为

cpp 复制代码
{
md5:32位md5值
file:<二进制文件>
}

这是 Mongoose 轻量级 C 网络库 专门用来处理 HTTP 协议的核心数据结构,也是「校准文件上传接口」的底层基础。

简单说:Mongoose 帮你把复杂的原始 HTTP 二进制数据,自动解析成了这些 C 语言能直接访问的结构化对象,你不用自己手写 HTTP 协议解析器。

cpp 复制代码
// Describes an arbitrary chunk of memory
struct mg_str {
  char *buf;   // String data
  size_t len;  // String length
};
1. struct mg_http_header:单个 HTTP 请求头
作用:存储一个 HTTP 头的键值对,比如 Content-Type: multipart/form-data
struct mg_http_header {
  struct mg_str name;   // HTTP头的名字,比如 "Content-Type"、"Authorization"
  struct mg_str value;  // HTTP头的值,比如 "multipart/form-data"
};

2. struct mg_http_message:完整的 HTTP 请求 / 响应(核心)
作用:Mongoose 收到一个 HTTP 请求后,会把整个请求自动解析成这个结构体,后面代码所有的 HTTP 接口逻辑都是基于它的。
后面接口int load_config_calibration_file(const struct mg_str& body)的这个 body 参数,就是从 mg_http_message.body 传过来的!之后所有的文件解析逻辑,都是在处理这个字段里的二进制数据。

struct mg_http_message {
  struct mg_str method, uri, query, proto;             // Request/response line
  struct mg_http_header headers[MG_MAX_HTTP_HEADERS];  // Headers
  struct mg_str body;                                  // Body
  struct mg_str head;                                  // Request + headers
  struct mg_str message;  // Request + headers + body
};


//-----------------------------------------
3.struct mg_http_part 作用:专门用来解析 multipart/form-data 格式的表单数据(也就是文件上传格式)。
mongoose库的mg_http_next_multipart 函数,就是把 mg_http_message.body 里的二进制数据,拆分成一个个struct mg_http_part结构体。

struct mg_http_part {
  struct mg_str name;      // 表单字段的name属性,对应前端input的name
                           // 比如代码里的 "file" 和 "md5"
  struct mg_str filename;  // 如果是文件上传字段,这里存上传的原始文件名
                           // 普通文本字段的话,这个字段的len是0
  struct mg_str body;      // 分段的内容:
                           // - 文件字段:存文件的二进制数据
                           // - 文本字段:存文本内容
};
以下这段截取自int load_config_calibration_file(const struct mg_str& body){}接口的代码就是如何解析mg_http_part 的示例
struct mg_http_part part;
while ((ofs = mg_http_next_multipart(body, ofs, &part)) != 0) {
    std::string name(part.name.buf, part.name.len);
    if (part.filename.len > 0) {
        // 这是文件字段
        std::vector<char> file_content(part.body.buf, part.body.buf + part.body.len);
    } else {
        // 这是文本字段
        std::string value(part.body.buf, part.body.len);
    }
}
//--------------------------------------------------
cpp 复制代码
void deleteDirectory(const std::string& path) {
        DIR* dir = opendir(path.c_str());
        if (!dir) {
            return;
        }

        struct dirent* entry;
        while ((entry = readdir(dir)) != nullptr) {
            const std::string name = entry->d_name;
            if (name == "." || name == "..") {
                continue;
            }

            std::string fullPath = path + "/" + name;
            struct stat statbuf;
            if (stat(fullPath.c_str(), &statbuf) == -1) {
                continue;
            }

            if (S_ISDIR(statbuf.st_mode)) {
                deleteDirectory(fullPath);
                rmdir(fullPath.c_str());
            } else {
                unlink(fullPath.c_str());
            }
        }
        closedir(dir);
    }


    bool createDirectory(const std::string& path) {
        return mkdir(path.c_str(), 0766) == 0; // Linux/Unix权限设置
    }

    // 检查目录是否存在
    bool dirExists(const std::string& path) {
        struct stat info;
        return stat(path.c_str(), &info) == 0 && (info.st_mode & S_IFDIR);
    }

    bool checkDir(const std::string& path) {
        std::string dirpath = path.substr(0, path.find_last_of("/\\"));
        if (!dirExists(dirpath)) {
            if (!createDirectory(dirpath)) {
                std::cerr << "Failed to create directory: " << dirpath << std::endl;
                return false;
            }
        }
        return true;
    }

    bool saveFile(const std::string& filepath, const std::vector<char>& data) {
        std::ofstream file(filepath, std::ios::binary);
        if (!file.is_open()) {
            std::cerr << "Cannot open file for writing: " << filepath << std::endl;
            return false;
        }
        file.write(data.data(), data.size());
        file.close();
        return !file.fail();
    }

    int load_config_calibration_file(const struct mg_str& body) {
        const char* TARGET_FILE = "/usr/share/zcqdemo/ConfigureUniCalibrationMap.csv";
        const char* TARGET_DIR = "/usr/share/zcqdemo";
        // 解析 multipart/form-data
        std::map<std::string, std::string> text_fields;
        std::map<std::string, std::vector<char>> file_data;
        std::map<std::string, std::string> filenames;
        // 使用mongoose的multipart解析函数,循环解析每个part
        struct mg_http_part part;
        size_t ofs = 0;

        while ((ofs = mg_http_next_multipart(body, ofs, &part)) != 0) {
            std::string name(part.name.buf, part.name.len);

            if (part.filename.len > 0) {
                std::string filename(part.filename.buf, part.filename.len);
                std::vector<char> file_content(part.body.buf, part.body.buf + part.body.len);
                file_data[name] = file_content;
                filenames[name] = filename;
                std::cout << "Found calibration file field: " << name
                        << ", filename: " << filename
                        << ", size: " << file_content.size() << " bytes" << std::endl;
            } else {
                // 文本字段
                std::string value(part.body.buf, part.body.len);
                text_fields[name] = value;
                std::cout << "Found calibration text field: " << name << " = " << value << std::endl;
            }
        }

        // 字段校验
        if (file_data.find("file") == file_data.end() ||
            filenames.find("file") == filenames.end() ||
            text_fields.find("md5") == text_fields.end())
        {
            std::cerr << "Calibration form data is missing! Required fields: file, md5" << std::endl;
            return -1;
        }

        std::string expected_md5 = text_fields["md5"];
        std::vector<char>& tmp_data = file_data["file"];
        // 计算文件实际MD5
        std::string actual_md5 = xcryto::get_md5(std::string(tmp_data.begin(), tmp_data.end()));
        if (actual_md5.length() != 32) {
            std::cerr << "Calibration file MD5 calculate error" << std::endl;
            return -1;
        }
        std::cout << "actual MD5: " << actual_md5 << std::endl;
        if (actual_md5 != expected_md5) {
            std::cerr << "Error: MD5 mismatch!" << std::endl;
            std::cerr << "Expected:" << expected_md5 << std::endl;
            std::cerr << "Actual:" << actual_md5 << std::endl;
            return -1;
        }
        std::cout << "MD5 verification passed" << std::endl;
        // 写入文件到目标路径
        std::lock_guard<std::mutex> lock(calibration_map_mutex);
        if (!checkDir(TARGET_DIR)) {
            std::cerr << "Error: Failed to create directory: " << TARGET_DIR << std::endl;
            return -1;
        }
        // 删除目录下所有文件
        deleteDirectory(TARGET_DIR);
        // 直接写入二进制内容
        if (!saveFile(TARGET_FILE, tmp_data)) {
            std::cerr << "Error: Failed to save calibration file: " << TARGET_FILE << std::endl;
            return -1;
        }
        std::cout << "Calibration file saved successfully: " << TARGET_FILE << std::endl;

        // 清空内存表,重新加载,当前已经持有 calibration_map_mutex,传 true
        calibration_map.clear();
        int reload_ret = ConfigureCalibrationMap(true);
        if (reload_ret != 0) {
            std::cout << "Error: Failed to reload calibration map" << std::endl;
            return -1;
        }

        std::cout << "Calibration map updated, total entries:" << calibration_map.size() << std::endl;
        return 0;
    }

「HTTP表单上传校准文件」接口前端网页通用的 multipart/form-data 标准格式,可以直接用浏览器上传文件。


一、整体功能

接收前端网页通过表单上传的**MD5校验值 + 校准文件(二进制流) **,解析标准HTTP上传格式,校验MD5完整性,清空旧校准目录,保存新文件到Linux系统,最后重新加载校准表到内存生效,全程加锁保证多线程安全。


二、先讲新增的核心工具函数:递归删除目录

cpp 复制代码
// 功能:递归删除一个目录及其下所有文件和子目录(Linux系统专用)
void deleteDirectory(const std::string& path) {
    // 1. 打开目录
    DIR* dir = opendir(path.c_str());
    if (!dir) { // 目录打不开(不存在/权限不够)直接返回
        return;
    }

    struct dirent* entry;
    // 2. 循环读取目录里的每一个条目
    while ((entry = readdir(dir)) != nullptr) {
        const std::string name = entry->d_name;
        
        // 跳过当前目录(.)和上级目录(..),这两个是Linux目录的默认条目
        if (name == "." || name == "..") {
            continue;
        }

        // 拼接成完整路径:父目录/文件名
        std::string fullPath = path + "/" + name;
        struct stat statbuf;
        // 获取这个条目的信息(是文件还是目录)
        if (stat(fullPath.c_str(), &statbuf) == -1) {
            continue;
        }

        // 3. 如果是子目录 → 递归调用自己删除子目录
        if (S_ISDIR(statbuf.st_mode)) {
            deleteDirectory(fullPath);  // 先删子目录里的所有内容
            rmdir(fullPath.c_str());    // 再删空的子目录本身
        } 
        // 4. 如果是普通文件 → 直接删除文件
        else {
            unlink(fullPath.c_str());
        }
    }

    closedir(dir); // 关闭目录句柄
}

为什么需要这个? 上传新校准文件前,要把旧目录里的所有残留文件全部删掉,保证目录里只有最新的校准文件,不会有旧文件干扰。


三、其他工具函数

cpp 复制代码
/**
 * @brief 在Linux系统创建单个目录
 * @param path 要创建的目录路径
 * @return 创建成功返回true,失败返回false
 */
bool createDirectory(const std::string& path) {
    // 调用Linux系统函数mkdir创建目录
    // 第二个参数0766是权限:所有者可读可写可执行,其他用户可读可写
    // mkdir成功返回0,失败返回-1,所以等于0就是成功
    return mkdir(path.c_str(), 0766) == 0;
}

/**
 * @brief 检查指定路径是否存在,并且是一个目录(不是文件)
 * @param path 要检查的路径
 * @return 存在且是目录返回true,否则返回false
 */
bool dirExists(const std::string& path) {
    // stat结构体,存储文件/目录的属性
    struct stat info;
    // stat系统调用:获取path的属性,成功返回0
    // info.st_mode & S_IFDIR:按位与判断该路径是否为目录类型
    return stat(path.c_str(), &info) == 0 && (info.st_mode & S_IFDIR);
}

/**
 * @brief 保证文件的父目录一定存在,不存在则自动递归创建
 * @param path 完整的文件路径(比如/usr/share/test.csv)
 * @return 父目录存在或创建成功返回true,失败返回false
 */
bool checkDir(const std::string& path) {
    // 从完整文件路径中截取父目录:找到最后一个/或\的位置,截取前面的部分
    // 比如"/usr/share/test.csv" → 截取后得到"/usr/share"
    std::string dirpath = path.substr(0, path.find_last_of("/\\"));
    
    // 检查父目录是否存在
    if (!dirExists(dirpath)) {
        // 不存在则创建父目录
        if (!createDirectory(dirpath)) {
            std::cerr << "创建目录失败: " << dirpath << std::endl;
            return false;
        }
    }
    return true;
}

/**
 * @brief 以二进制模式将数据写入文件,保证内容和原始数据完全一致
 * @param filepath 要写入的文件路径
 * @param data 要写入的二进制数据
 * @return 写入成功返回true,失败返回false
 */
bool saveFile(const std::string& filepath, const std::vector<char>& data) {
    // 以二进制模式打开文件(std::ios::binary)
    // 二进制模式不会自动转换换行符,避免破坏二进制文件内容
    std::ofstream file(filepath, std::ios::binary);
    
    // 检查文件是否成功打开(失败原因:权限不足、磁盘满、路径不存在)
    if (!file.is_open()) {
        std::cerr << "无法打开文件写入: " << filepath << std::endl;
        return false;
    }
    
    // 将vector中的所有二进制数据写入文件
    // data.data()返回vector内部数据的指针,data.size()返回数据长度
    file.write(data.data(), data.size());
    // 关闭文件,刷新缓冲区到磁盘
    file.close();
    
    // 检查写入过程中是否发生错误,无错误返回true
    return !file.fail();
}

四、核心函数:HTTP表单上传校准文件

标准HTTP multipart/form-data 格式 ,前端可以直接用<form>标签上传文件。

约定的前端表单格式

html 复制代码
<form action="/upload_calibration" method="post" enctype="multipart/form-data">
		<input type="text" name="md5" placeholder="文件MD5值"> <!-- 文本字段,name必须是"md5" -->
    <input type="file" name="file" accept=".csv"> <!-- 文件字段,name必须是"file" -->
    <button type="submit">上传</button>
</form>

逐行解析核心函数

cpp 复制代码
// 功能:处理HTTP上传的校准文件请求
// 参数 body:mongoose网络库的请求体结构体,包含完整的multipart数据
int load_config_calibration_file(const struct mg_str& body) {
    // 目标文件和目录路径
    const char* TARGET_FILE = "/usr/share/zcqdemo/ConfigureUniCalibrationMap.csv";
    const char* TARGET_DIR = "/usr/share/zcqdemo";

    // 定义三个map,用来存解析出来的内容
    std::map<std::string, std::string> text_fields;  // 存文本字段(比如md5)
    std::map<std::string, std::vector<char>> file_data; // 存文件内容
    std::map<std::string, std::string> filenames;    // 存上传的文件名

    struct mg_http_part part; // mongoose的multipart分段结构体
    size_t ofs = 0;           // 解析偏移量,从0开始

第一步:解析标准multipart/form-data格式

cpp 复制代码
    // 循环解析每一个multipart分段
    // mg_http_next_multipart:mongoose库自带的标准解析函数
    // 返回0表示解析完成,否则返回下一个分段的偏移量
    while ((ofs = mg_http_next_multipart(body, ofs, &part)) != 0) {
        // 获取当前分段的name属性(对应前端input的name)
        std::string name(part.name.buf, part.name.len);

        // 如果有filename属性 → 这是一个文件分段
        if (part.filename.len > 0) {
            // 获取上传的文件名
            std::string filename(part.filename.buf, part.filename.len);
            // 把文件的二进制内容存到vector里
            std::vector<char> file_content(part.body.buf, part.body.buf + part.body.len);
            
            file_data[name] = file_content;
            filenames[name] = filename;
            
            std::cout << "找到文件字段: " << name
                    << ", 文件名: " << filename
                    << ", 大小: " << file_content.size() << "字节" << std::endl;
        } 
        // 没有filename → 这是一个普通文本分段
        else {
            // 获取文本内容
            std::string value(part.body.buf, part.body.len);
            text_fields[name] = value;
            
            std::cout << "找到文本字段: " << name << " = " << value << std::endl;
        }
    }

这是整个函数最核心的部分 :multipart格式会把表单里的每个字段(文件和文本)都分成一个独立的分段,mongoose的mg_http_next_multipart会自动帮你拆分这些分段,不用自己手动解析二进制格式。


第二步:校验必填字段

cpp 复制代码
    // 检查必须的字段是否都存在:file(文件)和 md5(文本)
    if (file_data.find("file") == file_data.end() ||
        filenames.find("file") == filenames.end() ||
        text_fields.find("md5") == text_fields.end())
    {
        std::cerr << "表单数据缺失!必须字段:file, md5" << std::endl;
        return -1;
    }

第三步:MD5完整性校验

cpp 复制代码
    // 取出前端传过来的MD5值和文件内容
    std::string expected_md5 = text_fields["md5"];
    std::vector<char>& tmp_data = file_data["file"];

    // 计算上传文件的实际MD5值
    std::string actual_md5 = xcryto::get_md5(std::string(tmp_data.begin(), tmp_data.end()));
    
    if (actual_md5.length() != 32) {
        std::cerr << "MD5计算失败" << std::endl;
        return -1;
    }
    
    std::cout << "实际MD5: " << actual_md5 << std::endl;
    
    // 对比MD5,不一致说明文件损坏或被篡改
    if (actual_md5 != expected_md5) {
        std::cerr << "错误:MD5不匹配!" << std::endl;
        std::cerr << "期望:" << expected_md5 << std::endl;
        std::cerr << "实际:" << actual_md5 << std::endl;
        return -1;
    }

    std::cout << "MD5校验通过" << std::endl;

第四步:文件操作 + 内存更新(全程加锁)

cpp 复制代码
    // 【重要】提前加互斥锁,保证整个文件操作+内存更新是原子的
    // 防止其他线程在这个过程中读取校准表,读到脏数据
    std::lock_guard<std::mutex> lock(calibration_map_mutex);

    // 1. 保证目标目录存在
    if (!checkDir(TARGET_DIR)) {
        std::cerr << "错误:创建目录失败: " << TARGET_DIR << std::endl;
        return -1;
    }

    // 2. 清空目标目录下的所有旧文件(递归删除)
    deleteDirectory(TARGET_DIR);

    // 3. 保存新的校准文件
    if (!saveFile(TARGET_FILE, tmp_data)) {
        std::cerr << "错误:保存校准文件失败: " << TARGET_FILE << std::endl;
        return -1;
    }

    std::cout << "校准文件保存成功: " << TARGET_FILE << std::endl;

    // 4. 清空内存里的旧校准表,重新加载新的
    calibration_map.clear();
    // 传true表示:当前已经持有calibration_map_mutex,函数内部不用再加锁
    int reload_ret = ConfigureCalibrationMap(true);
    
    if (reload_ret != 0) {
        std::cout << "错误:重新加载校准表失败" << std::endl;
        return -1;
    }

    std::cout << "校准表更新成功,总条目数:" << calibration_map.size() << std::endl;
    return 0; // 整个流程成功
}

五、完整流程时序图

复制代码
前端提交表单(MD5 + file)
    ↓
后端收到multipart格式的请求体
    ↓
循环解析每个分段,分离文本和文件
    ↓
校验file和md5字段是否存在
    ↓
计算文件实际MD5,和前端传的对比
    ↓
加互斥锁,进入原子操作
    ↓
创建目标目录
    ↓
递归删除目录下所有旧文件
    ↓
保存新的校准文件到磁盘
    ↓
清空内存旧校准表,重新加载新文件
    ↓
解锁,返回成功

七、关键知识点总结

  1. multipart/form-data:HTTP文件上传的标准格式,把多个字段分成独立的分段传输
  2. 递归删除目录 :先删子目录内容,再删目录本身,必须跳过...
  3. 提前加锁:把整个文件操作+内存更新都放在锁里,保证原子性,避免并发问题
  4. ConfigureCalibrationMap(true):传true表示外部已经加锁,内部不用重复加锁,避免死锁
  5. 标准格式的优势:通用性强,前端开发成本低,不容易出解析错误
相关推荐
零壹AI实验室2 小时前
DeepSeek本地部署:从零开始,把大模型跑在自己电脑上
服务器·网络·人工智能·电脑
小船跨境2 小时前
2026 Google代理指南:如何安全获取搜索数据?
网络协议·tcp/ip·安全
IpdataCloud2 小时前
游戏安全运营中,如何用IP代理识别服务快速检测作弊网络出口?操作指南来了
运维·网络·tcp/ip·安全·游戏
浪客灿心2 小时前
Linux数据链路层
linux·网络
wanhengidc2 小时前
服务器机柜的功能是什么
运维·服务器·网络
2301_780789662 小时前
容器环境漏洞扫描:适配 K8s 架构的镜像与 Pod 安全检测方案
网络·安全·web安全·云原生·架构·kubernetes·ddos
小明同学012 小时前
计算机网络编程———手写 TCP 服务器(一)搞懂网络编程核心 API
服务器·网络·计算机网络
广州创科水利2 小时前
广州创科:以硬核科技与全栈能力,守护边坡安全监测防线
大数据·网络·人工智能
许长安3 小时前
RingBuffer:面向网络编程的环形缓冲区实现
服务器·网络·c++·经验分享·笔记·缓存