解析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,和前端传的对比
↓
加互斥锁,进入原子操作
↓
创建目标目录
↓
递归删除目录下所有旧文件
↓
保存新的校准文件到磁盘
↓
清空内存旧校准表,重新加载新文件
↓
解锁,返回成功
七、关键知识点总结
- multipart/form-data:HTTP文件上传的标准格式,把多个字段分成独立的分段传输
- 递归删除目录 :先删子目录内容,再删目录本身,必须跳过
.和.. - 提前加锁:把整个文件操作+内存更新都放在锁里,保证原子性,避免并发问题
- ConfigureCalibrationMap(true):传true表示外部已经加锁,内部不用重复加锁,避免死锁
- 标准格式的优势:通用性强,前端开发成本低,不容易出解析错误