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

寻求支持边下载边解压tar.gz的C/C++库及libarchive问题排查

边下载边解压tar.gz文件的C/C++实现方案

我想用libcurl下载tar.gz文件,实现边下载边解压——每下载一段数据块就立即解压,不用等整个文件下载完再统一解压。有没有能满足这个需求的C/C++库?

我试过用libarchive解压,但处理第一个数据块时返回了truncated gzip input错误,看起来libarchive好像需要完整文件才能解压。下面是我的代码,刚接触libarchive,不确定用法对不对:

#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <atomic>
#include <thread>

// libarchive
#include <archive.h>
#include <archive_entry.h>
#include <curl/curl.h>

struct mydata {
  void *buffer;
  ssize_t *size;
};
struct curldata {
    void *buffer;
    ssize_t *size;
    CURL *curl;
};

std::atomic<bool> rd(true);
struct archive *archive, *archivefd;
std::atomic<bool> start_read(false);
la_ssize_t libarchiveRead(struct archive* a, void* client_data, const void** block)
{
    if(!rd) {
    mydata *my_data = (mydata*)client_data;
    std::cout << "calling custom read(), size " << *(my_data->size) << std::endl;
    *block = my_data->buffer;
    rd=true;
    return *(my_data->size);
    }
    return 0;
}

int libarchiveClose(struct archive* a, void* client_data)
{
    std::cout << "calling custom close() for archive" << std::endl;
    mydata *my_data = (mydata*)client_data;
    delete my_data;
    return (ARCHIVE_OK);
}

int libarchiveClosefd(struct archive* a, void* client_data)
{
    std::cout << "calling custom close() for archivefd" << std::endl;
    mydata *my_data = (mydata*)client_data;
    delete my_data;
    return (ARCHIVE_OK);
}
static size_t curlWriteFunction(void *ptr, size_t size, size_t nmemb, void *write_data) {
    //size is always 1
    curldata *my_data = (curldata*)(write_data);
    *(my_data->size) = nmemb * size;
    std::cout << "calling curlWriteFunction(), size: " << size << " , nmemb: " << nmemb
    << " , my_data->size: " << *(my_data->size) << std::endl;
    memcpy(my_data->buffer, ptr, *(my_data->size));
    curl_easy_pause(my_data->curl, CURL_WRITEFUNC_PAUSE);
    rd=false;
    return (*(my_data->size));
}


 static size_t progress(void *clientp, double dltotal, double dlnow, double ultotal, double ulnow) {
    CURL *curl = (CURL *)clientp;
    (void)ultotal;
    (void)ulnow;
    if(dltotal == 0) {
        return 0;
    }
    if(rd) {

        curl_easy_pause(curl, CURLPAUSE_CONT);
        std::cout << "progress: " << dlnow/dltotal * 100 << "%" << std::endl;
    }
    return 0;
 }

void readarchive(void *client_data) {
    struct archive_entry    *entry;
    int flags = ARCHIVE_EXTRACT_TIME;
    flags |= ARCHIVE_EXTRACT_PERM;
    flags |= ARCHIVE_EXTRACT_ACL;
    flags |= ARCHIVE_EXTRACT_FFLAGS;
    while(rd);
    std::cout << "calling archive_read_open for archive.." << std::endl;
    int res = archive_read_open(archive,
                                client_data,
                                nullptr,
                                (archive_read_callback*)libarchiveRead,
                                (archive_close_callback*)libarchiveClose);
    std::cout << "called archive_read_open for archive.." << std::endl;
    res = archive_read_next_header(archive, &(entry));
    while(res == ARCHIVE_OK ) {
        std::cout << "Extracting for archive " << archive_entry_pathname(entry) << "..." << std::endl;
        // extract current entry
        archive_read_extract(archive, entry, flags);
        // read next if available
        res = archive_read_next_header(archive, &(entry));
    }
    std::cout << "archive_read_next_header for archive failed, errcode: " << res << " error: " << archive_error_string(archive) << std::endl;
}

//size_t curlWriteFunction(void *ptr, size_t size, size_t nmemb,FILE* fptr) {
//    //size is always 1
//    std::cout << "calling curlWriteFunction().." << std::endl;
//    return fwrite(ptr, size, nmemb, fptr);
//}
int main(int argc, char** argv) {

    if(argc < 3)
    {
        std::cout << argv[0] << "{-r | -w} file[s]" << std::endl;
        return 1;
    }

    std::vector<std::string> filenames;
    filenames.reserve(argc);

    while (*++argv != nullptr)
    {
        filenames.emplace_back(*argv);
    }

    bool modeRead = (filenames[0] == "-r");
    std::cout << filenames[0] << " " << filenames[1] << std::endl;

    // archive related variables

    char buff_archive[16 * 1024], buff_archivefd[16 * 1024];

    if(modeRead)
    {

        archive = archive_read_new();
        archive_read_support_filter_gzip(archive);
        archive_read_support_format_tar(archive);

        mydata *client_data = new mydata();
        int res;
        char *buff1 = new char[16 * 1024];

        client_data->size = new ssize_t;
        *(client_data->size) = 0;
        client_data->buffer = buff1;

        curldata *curl_data = new curldata();
        curl_data->size=client_data->size;
        curl_data->buffer=buff1;

        CURL *curl = curl_easy_init();
        curl_data->curl = curl;
        curl_easy_setopt(curl, CURLOPT_URL, filenames[1].c_str());
        curl_easy_setopt(curl, CURLOPT_WRITEDATA, curl_data);
        curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curlWriteFunction);
        curl_easy_setopt(curl, CURLOPT_NOPROGRESS, 0L);
        curl_easy_setopt(curl, CURLOPT_PROGRESSDATA, curl);
        curl_easy_setopt(curl, CURLOPT_PROGRESSFUNCTION,progress);
        std::thread t(readarchive, client_data);
        CURLcode result = curl_easy_perform(curl);
        if(result != CURLE_OK) {
            std::cout << "curl perform failed, errcode; " << result << " err: " << curl_easy_strerror(result) << std::endl;
        }
        t.join();
        delete client_data->size;
        delete []buff1;
        archive_read_close(archive);
        archive_read_free(archive);
        archive_read_free(archive);
        curl_easy_cleanup(curl);
    }


    return 0;

}

问题分析与解决方案

libarchive本身完全支持流式解压,你遇到的错误是因为代码里的数据流衔接逻辑错误:

  1. 自定义libarchiveRead回调只返回了第一块数据就返回0,相当于告诉libarchive输入已经结束,导致它判定gzip数据被截断
  2. 线程同步逻辑不合理,下载和解压的数据流没有持续对接,无法给libarchive提供后续的下载数据

正确的实现方式是用线程安全的缓冲区队列来衔接下载和解压线程,核心思路:

  • 下载线程(curl)把收到的数据块存入队列
  • 解压线程(libarchive)从队列里持续取数据,没有数据时阻塞等待
  • 下载完成后标记队列结束,让解压线程处理完剩余数据再退出

以下是修改后的完整代码:

#include <iostream>
#include <vector>
#include <string>
#include <unistd.h>
#include <fcntl.h>
#include <string.h>
#include <atomic>
#include <thread>
#include <queue>
#include <mutex>
#include <condition_variable>

#include <archive.h>
#include <archive_entry.h>
#include <curl/curl.h>

// 线程安全的下载缓冲区,用于衔接curl和libarchive
struct DownloadBuffer {
    std::queue<std::vector<char>> chunks;
    std::mutex mtx;
    std::condition_variable cv;
    bool download_finished = false;

    // 添加下载到的数据块
    void add_chunk(const char* data, size_t size) {
        std::lock_guard<std::mutex> lock(mtx);
        chunks.emplace(data, data + size);
        cv.notify_one();
    }

    // 获取数据块(无数据时阻塞,直到有新数据或下载结束)
    bool get_chunk(std::vector<char>& out) {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this](){ return !chunks.empty() || download_finished; });
        
        if (chunks.empty() && download_finished) {
            return false;
        }

        out.swap(chunks.front());
        chunks.pop();
        return true;
    }

    // 标记下载完成
    void finish() {
        std::lock_guard<std::mutex> lock(mtx);
        download_finished = true;
        cv.notify_one();
    }
};

// libarchive自定义读取回调:从DownloadBuffer获取数据
la_ssize_t libarchive_read_callback(struct archive* a, void* client_data, const void** block) {
    DownloadBuffer* buffer = static_cast<DownloadBuffer*>(client_data);
    static std::vector<char> current_chunk;

    if (current_chunk.empty()) {
        if (!buffer->get_chunk(current_chunk)) {
            return ARCHIVE_EOF;
        }
    }

    *block = current_chunk.data();
    la_ssize_t ret = current_chunk.size();
    current_chunk.clear();
    return ret;
}

// libarchive自定义关闭回调
int libarchive_close_callback(struct archive* a, void* client_data) {
    DownloadBuffer* buffer = static_cast<DownloadBuffer*>(client_data);
    delete buffer;
    return ARCHIVE_OK;
}

// curl自定义写入回调:把数据存入DownloadBuffer
static size_t curl_write_callback(void *ptr, size_t size, size_t nmemb, void *write_data) {
    DownloadBuffer* buffer = static_cast<DownloadBuffer*>(write_data);
    buffer->add_chunk(static_cast<const char*>(ptr), size * nmemb);
    return size * nmemb;
}

// 解压线程函数
void extract_archive_thread(DownloadBuffer* buffer) {
    struct archive* a = archive_read_new();
    archive_read_support_filter_gzip(a);
    archive_read_support_format_tar(a);

    // 设置解压选项:保留时间、权限、ACL等
    int flags = ARCHIVE_EXTRACT_TIME | ARCHIVE_EXTRACT_PERM | ARCHIVE_EXTRACT_ACL | ARCHIVE_EXTRACT_FFLAGS;

    // 绑定自定义数据源
    int res = archive_read_open(a, buffer, nullptr, libarchive_read_callback, libarchive_close_callback);
    if (res != ARCHIVE_OK) {
        std::cerr << "初始化libarchive失败: " << archive_error_string(a) << std::endl;
        archive_read_free(a);
        return;
    }

    struct archive_entry* entry;
    // 循环读取并解压每个文件
    while ((res = archive_read_next_header(a, &entry)) == ARCHIVE_OK) {
        std::cout << "正在解压: " << archive_entry_pathname(entry) << std::endl;
        archive_read_extract(a, entry, flags);
    }

    if (res != ARCHIVE_EOF) {
        std::cerr << "解压失败: " << archive_error_string(a) << std::endl;
    }

    archive_read_close(a);
    archive_read_free(a);
}

int main(int argc, char** argv) {
    if (argc != 3 || std::string(argv[1]) != "-r") {
        std::cout << "使用方式: " << argv[0] << " -r <tar.gz文件URL>" << std::endl;
        return 1;
    }

    // 初始化curl全局环境
    curl_global_init(CURL_GLOBAL_ALL);
    CURL* curl = curl_easy_init();
    if (!curl) {
        std::cerr << "初始化curl失败" << std::endl;
        return 1;
    }

    DownloadBuffer* buffer = new DownloadBuffer();

    // 设置curl下载参数
    curl_easy_setopt(curl, CURLOPT_URL, argv[2]);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, buffer);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, curl_write_callback);
    curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); // 跟随HTTP重定向

    // 启动解压线程
    std::thread extract_thread(extract_archive_thread, buffer);

    // 执行下载
    CURLcode result = curl_easy_perform(curl);
    if (result != CURLE_OK) {
        std::cerr << "下载失败: " << curl_easy_strerror(result) << std::endl;
    }

    // 标记下载完成,通知解压线程处理剩余数据
    buffer->finish();

    // 等待解压线程完成
    extract_thread.join();

    // 清理资源
    curl_easy_cleanup(curl);
    curl_global_cleanup();

    return 0;
}

代码关键点说明

  • DownloadBuffer类实现了线程安全的队列,用互斥锁和条件变量保证多线程下的数据同步
  • libarchive的读取回调会持续从队列取数据,直到收到ARCHIVE_EOF标记
  • curl的写入回调只管将数据存入队列,无需暂停下载,提升了整体效率
  • 下载完成后标记队列结束,确保解压线程能处理完所有剩余数据再退出

内容的提问来源于stack exchange,提问作者zlh121546

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.04 15:15:10