Libcurl回调函数多次调用致缓冲区数据截断及JSON值缺失问题
问题描述
桌面端运行Python服务器,负责解析大型CSV文件,响应HTTP请求时将每行数据以JSON格式发送至嵌入式设备。同一设备上用C程序测试正常,但部署到嵌入式Ubuntu设备后,出现接收数据随机缺失的情况。服务器已确认发送完整数据,但接收端的write_callback会被随机多次调用,拆分响应内容,导致数据截断、值缺失。
服务器端Python代码
self.send_response(200) self.send_header('Content-type', 'application/json') self.send_header('Content-Encoding', 'chunked') # Specify gzip encoding self.end_headers() response_data = self.read_csv_lines(param_value) response_json = json.dumps(response_data) # Compress the JSON data buf = io.BytesIO() with gzip.GzipFile(fileobj=buf, mode='w') as f: f.write(response_json.encode('utf-8')) compressed_data = buf.getvalue() # Send the compressed JSON response self.wfile.write(compressed_data)
客户端C代码
size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) { size_t realsize = size * nmemb; char *data = (char *)userp; // Append the received data to the user buffer memcpy(data, curl_buffer, realsize); return realsize; } void readDatasetLineByLine(CURL* curl ,CURLcode res ,int line_number){ // Read float values from file float value; char url_query[50]; sprintf(url_query,"http://192.168.137.1:8080?param=%d",line_number); curl_easy_setopt(curl, CURLOPT_URL, url_query); // Set the callback function to handle received data curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); // Create a buffer to store the received data // Perform the HTTP request res = curl_easy_perform(curl); test_input_memory[0] = value; if (res != CURLE_OK) { fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); } else { // Print the received text cJSON *root = cJSON_Parse(curl_buffer); if (root != NULL) { // Check if the root is an array if (cJSON_IsArray(root)) { // Iterate through the array and print the float values int count = 0; cJSON *value; cJSON_ArrayForEach(value, root) { if (cJSON_IsNumber(value)) { test_input_memory[count] = (float)value->valuedouble; count += 1; } } } // Free cJSON objects cJSON_Delete(root); } else { fprintf(stderr, "Error parsing JSON\n"); } }
问题根源分析
- 服务器响应头错误:代码中
Content-Encoding被错误设置为chunked,实际使用的是gzip压缩,导致curl无法正确识别压缩格式,处理逻辑混乱。 - C端回调函数逻辑错误:
memcpy(data, curl_buffer, realsize)参数完全颠倒,应该将接收到的contents复制到用户缓冲区,而非反向操作。- 未跟踪缓冲区已接收数据长度,每次回调都会覆盖之前的内容,分块接收时直接丢失历史数据。
- 未处理gzip解压:curl默认不会自动解压gzip响应,C端直接解析压缩后的二进制数据,必然导致JSON解析失败,表现为值缺失。
- 嵌入式网络特性:嵌入式设备带宽低、延迟高,HTTP响应更容易被拆分为多个TCP包传输,触发多次
write_callback,原代码未适配这种场景。
修复方案
1. 修正Python服务器响应头
将Content-Encoding改为正确的gzip,可选添加Content-Length头告知响应大小:
self.send_response(200) self.send_header('Content-type', 'application/json') self.send_header('Content-Encoding', 'gzip') # 修正为gzip压缩标识 self.send_header('Content-Length', str(len(compressed_data))) # 可选:告知响应大小 self.end_headers() response_data = self.read_csv_lines(param_value) response_json = json.dumps(response_data) # Compress the JSON data buf = io.BytesIO() with gzip.GzipFile(fileobj=buf, mode='w') as f: f.write(response_json.encode('utf-8')) compressed_data = buf.getvalue() # Send the compressed JSON response self.wfile.write(compressed_data)
2. 重构C端缓冲区与回调逻辑
使用带长度跟踪的动态缓冲区,确保分块数据被正确追加,同时配置curl自动解压gzip响应:
定义缓冲区结构体
typedef struct { char *buffer; size_t size; // 当前已接收数据长度 size_t capacity; // 缓冲区总容量 } ResponseBuffer;
修复write_callback函数
size_t write_callback(void *contents, size_t size, size_t nmemb, void *userp) { size_t realsize = size * nmemb; ResponseBuffer *buf = (ResponseBuffer *)userp; // 缓冲区容量不足时自动扩容(初始4KB,按需翻倍) if (buf->size + realsize > buf->capacity) { size_t new_cap = buf->capacity == 0 ? 4096 : buf->capacity * 2; char *new_buf = realloc(buf->buffer, new_cap); if (!new_buf) { fprintf(stderr, "Buffer realloc failed\n"); return 0; // 返回0终止curl传输 } buf->buffer = new_buf; buf->capacity = new_cap; } // 将新数据追加到缓冲区末尾 memcpy(buf->buffer + buf->size, contents, realsize); buf->size += realsize; return realsize; }
重构请求处理函数
void readDatasetLineByLine(CURL* curl, int line_number) { char url_query[50]; sprintf(url_query, "http://192.168.137.1:8080?param=%d", line_number); curl_easy_setopt(curl, CURLOPT_URL, url_query); // 配置curl自动解压gzip响应 curl_easy_setopt(curl, CURLOPT_ACCEPT_ENCODING, "gzip"); // 初始化动态缓冲区 ResponseBuffer buf = {NULL, 0, 0}; curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_callback); curl_easy_setopt(curl, CURLOPT_WRITEDATA, &buf); // 执行HTTP请求 CURLcode res = curl_easy_perform(curl); if (res != CURLE_OK) { fprintf(stderr, "curl_easy_perform() failed: %s\n", curl_easy_strerror(res)); } else { if (buf.size > 0) { // 添加字符串终止符,确保cJSON可以正确解析 if (buf.size + 1 > buf.capacity) { char *new_buf = realloc(buf.buffer, buf.capacity + 1); if (!new_buf) { fprintf(stderr, "Failed to add null terminator\n"); free(buf.buffer); return; } buf.buffer = new_buf; } buf.buffer[buf.size] = '\0'; // 解析JSON数据 cJSON *root = cJSON_Parse(buf.buffer); if (root != NULL) { if (cJSON_IsArray(root)) { int count = 0; cJSON *value; cJSON_ArrayForEach(value, root) { if (cJSON_IsNumber(value)) { // 确保test_input_memory有足够空间,避免越界 test_input_memory[count] = (float)value->valuedouble; count++; } } } cJSON_Delete(root); } else { fprintf(stderr, "JSON parse failed\n"); // 可选:打印原始数据排查问题 // fprintf(stderr, "Received data: %.*s\n", (int)buf.size, buf.buffer); } } else { fprintf(stderr, "No data received\n"); } } // 释放缓冲区内存,避免泄漏 free(buf.buffer); }
3. 额外优化建议
- 确保
test_input_memory数组大小足够容纳所有返回的数值,避免数组越界; - 嵌入式设备内存有限时,可根据预估数据量设置固定大小缓冲区,减少动态扩容开销;
- 服务器端可限制单条请求返回的数据量,避免嵌入式设备内存压力过大。
内容的提问来源于stack exchange,提问作者Random person
相关产品推荐
相关产品推荐

