65.9K
CodeProject 正在变化。 阅读更多。
Home

BCBCurl,一个基于 LibCurl 的下载管理器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.65/5 (7投票s)

2014年11月5日

CPOL

3分钟阅读

viewsIcon

24764

downloadIcon

1084

如何嵌入 LibCurl 来创建一个下载管理器。

源代码可在 https://bitbucket.org/pamungkas5/bcbcurl/ 获取

引言

Curl 是一个很棒的命令行工具,用于使用 URL 语法进行数据传输。它支持 FTP、FTPS、Gopher、HTTP、HTTPS、IMAP、IMAPS、LDAP、LDAPS、POP3 等等。它的库 (LibCurl) 被广泛应用于许多项目。
在本文中,我试图展示我在一个名为 BCBCurl 的简单下载管理器应用程序中嵌入 LibCurl 的方法。

使用的编译器:Embarcadero C++ Builder XE3

 

背景

以前我使用 curl.exe 命令行工具和 FreeDownloadManager 进行下载活动。每个都有自己的优点和缺点。

我开发这个程序的主要原因是因为我需要一个具有下表中所示功能的下载管理器

支持的功能 curl FDM BCBCurl
从命令行设置 URL
从命令行设置输出文件
从命令行设置引用页
从命令行设置 cookie
自动恢复中断的下载

下载队列
多线程下载 待办事项
Socks 协议 待办事项

使用代码

该程序有两个线程,主线程负责用户界面,工作线程负责下载。 我使用 "easy" libcurl API 进行下载。 这是一个阻塞套接字,所以我将它分离到不同的线程中。 来自下载器线程的消息使用 FIFO 列表传递到主线程。 LibCurl 实际上有非阻塞函数,但我还没有尝试使用它们。

BCBCurl 可以以两种不同的模式运行,第一种它可以作为实际下载并保持活动的服务器运行,第二种作为仅从命令行接收任务并将其发送到服务器的下载队列然后退出的客户端运行。 命令通过共享内存从客户端发送到服务器。 当你从脚本调用 BCBCurl 时,此选项非常有用,你可以选择等待下载完成或直接将其放入队列中。

这是调用 LibCurl 下载机制的函数
(它基于 LibCurl 内存下载示例代码)

int do_curl(TThreadCurl *chunk, unsigned long range_from, unsigned long range_to, int headeronly) {
    chunk->status = CURL_STARTED;
  CURL *curl_handle;
  CURLcode res;
  curl_handle = curl_easy_init();

  /* specify URL to get */
  curl_easy_setopt(curl_handle, CURLOPT_URL, chunk->url.c_str());

  if (chunk->referer != "") {
    curl_easy_setopt(curl_handle, CURLOPT_REFERER, chunk->referer.c_str());
  }

  if (chunk->cookie != "") {
    curl_easy_setopt(curl_handle, CURLOPT_COOKIE, chunk->cookie.c_str());
  }

//  curl_easy_setopt(curl_handle, CURLOPT_TIMEOUT, 60);
    curl_easy_setopt(curl_handle, CURLOPT_LOW_SPEED_LIMIT, 1);

    if (chunk->invalid) {
        chunk->status = CURL_TERMINATED;
        return 0;
    }

    int len;
    char *unescaped = curl_easy_unescape(curl_handle, chunk->url.c_str(), chunk->url.Length(), &len);
    chunk->unescaped = unescaped;
    curl_free(unescaped);

  /* send header data to this function  */
    curl_easy_setopt(curl_handle, CURLOPT_HEADERFUNCTION, header_callback);
    curl_easy_setopt(curl_handle, CURLOPT_HEADERDATA, (void*) chunk);

    curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYPEER, 0L);
    curl_easy_setopt(curl_handle, CURLOPT_SSL_VERIFYHOST, 0L);

  /* send content to this function  */
  curl_easy_setopt(curl_handle, CURLOPT_WRITEFUNCTION, WriteMemoryCallback);

  /* we pass our 'chunk' struct to the callback function */
  curl_easy_setopt(curl_handle, CURLOPT_WRITEDATA, (void*) chunk);

    if(headeronly > 0) {
        curl_easy_setopt(curl_handle, CURLOPT_HEADER, 1);
        curl_easy_setopt(curl_handle, CURLOPT_NOBODY, 1);
    }

    /* range download, FROM-TO byte */
    if ((range_from + range_to) > 0) {
        AnsiString srange = AnsiString().sprintf("%ld-%ld", range_from, range_to);
        msglist->Add("\r\n---\r\nDownload range: " + srange);
        curl_easy_setopt(curl_handle, CURLOPT_RANGE, srange.c_str());
        chunk->start_byte = range_from;
        chunk->last_byte = range_from;
        chunk->stop_byte = range_to;
        chunk->size_downloaded = range_from;
    }
    else
    if (chunk->stop_byte > chunk->last_byte) {
        AnsiString autorange = AnsiString().sprintf("%ld-%ld", chunk->last_byte, chunk->stop_byte);
        msglist->Add("Autorange: " + AnsiString(autorange));
        curl_easy_setopt(curl_handle, CURLOPT_RANGE, autorange.c_str());
        chunk->last_byte = chunk->last_byte;
        chunk->stop_byte = chunk->stop_byte;
        chunk->size_downloaded = chunk->last_byte;
    }
    else
    if (chunk->stop_byte > 0 && chunk->last_byte > 0) {
        msglist->Add("nothing to do");
        chunk->status = CURL_TERMINATED;
        return 0;
    }
  /* some servers don't like requests that are made without a user-agent
     field, so we provide one */
  curl_easy_setopt(curl_handle, CURLOPT_USERAGENT, "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.7.8) Gecko/20050511 Firefox/1.0.4");

  /* get it! */
    /* perform curl download */
  res = curl_easy_perform(curl_handle);

  /* check for errors */
  if(res != CURLE_OK) {
    msglist->Add("Err: " + AnsiString(curl_easy_strerror(res)));
    chunk->status = CURL_TERMINATED;
  }
  

  /* cleanup curl stuff */
  curl_easy_cleanup(curl_handle);

    chunk->status = CURL_TERMINATED;
    return res;
}

msglist 是一个 TList 对象,用作 FIFO 缓冲区以将消息发送到服务器。

chunk (TThreadCurl 对象) 包含从 Web 服务器下载的数据块和一些下载参数。 memory 变量根据 Content Length 标头动态分配。

class TThreadCurl : public TThread
{
public:
  char *memory; // to store downloaded data
  size_t size_downloaded; // size of downloaded data of a single do_curl call
  size_t size_memory; // total data download (from all do_curl call)
  unsigned long content_length; // content length of data from header
  unsigned long start_byte; // mark of starting point of download
  unsigned long stop_byte; // mark of ending point of download
  unsigned long last_byte; // last position of download
  int can_resume; // set if download can resume
  int invalid; // set if error encountered during download
  AnsiString url; // download URL
  AnsiString unescaped; // unescaped string of URL
  AnsiString referer; // referer from browser
  AnsiString cookie; // cookie from browser
  int status; // download status
// char *str_status[] = {"[UNKNOWN 0]", "[CURL STARTED]", "[CURL RECEIVING]", "[CURL TERMINATED]", "[UNKNOWN 5]"};
  int http_code; // http return code
  int download_step; // progress of download
...
}

如果从浏览器调用 BCBCurl,则可以从命令行参数设置 referercookie 。 我使用 Flashgot 插件从 Firefox 中调用 BCBCurl。

Libcurl 下载过程使用两个函数

1. header_callback() 处理来自 Web 服务器的标头。

2. WriteMemoryCallback() 从 Web 服务器捕获下载的内容。

header_callback 函数

当 LibCurl 从 Web 服务器下载标头数据时,将调用此函数。

size_t header_callback(char *buffer,   size_t size,   size_t nmemb,   void *userdata)
{
    TPerlRegEx *pcre = new TPerlRegEx();
    TThreadCurl *curl = (TThreadCurl *)userdata;
    pcre->RegEx = "(ACCEPT-RANGE)";
    pcre->Options = TPerlRegExOptions() << preCaseLess;
    pcre->Subject = AnsiString(buffer);//.UpperCase();
    if (pcre->Match()) {
        msglist->Add(buffer);
        curl->can_resume = 1;
    }

    pcre->RegEx = "HTTP\\/.*\\s*(\\d\\d\\d)";
    pcre->Options = TPerlRegExOptions() << preCaseLess;
    pcre->Subject = AnsiString(buffer);//.UpperCase();
    if (pcre->Match()) {
        curl->http_code = StrToInt(pcre->Groups[1]);
        if (curl->http_code > 206) {
            curl->invalid = 1;// cancel download
        }
    }

    pcre->RegEx = "Content-Disposition:";
    pcre->Options = TPerlRegExOptions() << preCaseLess;
    pcre->Subject = AnsiString(buffer);//.UpperCase();
    if (pcre->Match()) {
        pcre->RegEx = "Content-Disposition:.*filename=[\"\'](.*)[\"\']";
        pcre->Options = TPerlRegExOptions() << preCaseLess;
        pcre->Subject = AnsiString(buffer);//.UpperCase();
        if (pcre->Match()) {
            curl->header_filename = pcre->Groups[1];
        } else {
            pcre->RegEx = "Content-Disposition:.*filename=\\s*([^\\s]+)[\\s$]";
            pcre->Options = TPerlRegExOptions() << preCaseLess;
            pcre->Subject = AnsiString(buffer);//.UpperCase();
            if (pcre->Match()) {
                curl->header_filename = pcre->Groups[1];
            }
        }
    }

    pcre->RegEx = "CONTENT-LENGTH\\s*:\\s*(\\d+)";
    pcre->Options = TPerlRegExOptions() << preCaseLess;
    pcre->Subject = AnsiString(buffer);//.UpperCase();
    if (pcre->Match()) {
        msglist->Add(" --- CONTENT LENGTH: " + pcre->Groups[1]);
        unsigned long ctlen = StrToInt(pcre->Groups[1]);

        // CHECK HTTP PROTOCOL, WHY SUBSEQUENT CONTENT LENGTH REDUCED WITH DOWNLOADED SIZE
        if (ctlen > curl->content_length) {
            curl->content_length = ctlen;
        }
        size_t total_chunk_size = curl->content_length + 1;
        curl->memory = (char *) realloc(curl->memory, total_chunk_size);
        curl->size_memory = total_chunk_size;
        curl->stop_byte = curl->content_length;
        if(curl->memory == NULL) {
        /* out of memory! */
            msglist->Add("not enough memory (realloc returned NULL)\n");
        return 0;
      }
    }
    size_t realsize = size * nmemb;
    return  realsize;
}

我需要解析来自 Web 服务器的标头数据,以获取 Content-Length、(可选)文件名、HTTP 响应代码,并检查服务器是否支持恢复下载。 请注意我是如何使用 Perl 正则表达式来解析标头数据的。 我知道可以使用其他方法解析文本,但曾经是一名 Perl 程序员,我无法离开正则表达式 :) 。

WriteMemoryCallback 函数

当 LibCurl 从 Web 服务器下载实际数据时,将调用此函数。 它主要从函数参数中捕获数据并将它们存储在内存中的正确位置。

size_t WriteMemoryCallback(void *contents, size_t size, size_t nmemb, void *userp)
{
  size_t realsize = size * nmemb;
  TThreadCurl *curl = (TThreadCurl *)userp;
  curl->status = CURL_RECEIVING;
  size_t total_chunk_size = curl->size_downloaded + realsize + 1;

  if (curl->size_memory < total_chunk_size) {
    curl->memory = (char *) realloc(curl->memory, total_chunk_size);
    curl->size_memory = total_chunk_size;
  }

  if(curl->memory == NULL) {
    /* out of memory! */
    msglist->Add("not enough memory (realloc returned NULL)\n");
    return 0;
  }
  unsigned long offset = curl->size_downloaded > 0 ? curl->size_downloaded : 0;
  memcpy(&(curl->memory[offset]), contents, realsize);
  curl->last_byte = curl->last_byte + realsize;
//  msglist->Add("last byte: " + AnsiString(curl->last_byte) + "( " + AnsiString(100 * curl->last_byte / (curl->content_length > 0 ? curl->content_length : 1000)) + "% )");
  curl->size_downloaded += realsize;
  Application->ProcessMessages();
  return realsize;
}

命令行参数

BCBCurl 处理的命令行参数列表

  • -o [显式输出文件名]
  • -f [建议的文件名]
  • -d [输出文件夹]
  • -k [cookie]
  • -r [referer]
  • -m [评论]
  • -c (无值,如果只想将 BCBCurl 作为客户端运行,则设置)

浏览器集成

为了将 BCBCurl 与浏览器集成,我使用 FlashGot 插件并使用以下参数

-c [URL] [-r REFERER] [-k COOKIE] [-m COMMENT] [-f FNAME]

关注点

我通过制作这个程序学到的东西是

  1. 如何在桌面应用程序中使用 LibCurl。
  2. 如何解析命令行参数。
  3. 如何在 C++ 应用程序中使用 RegExp。
  4. 如何通过共享内存传递数据。
  5. 如何从浏览器调用 BCBCurl。

我知道我还没有完全解释它们以使这篇文章简短,但如果有人问,我很乐意解释。

BCBCurl 可执行文件包含在这篇文章中。 至于源代码,您可以从 https://bitbucket.org/pamungkas5/bcbcurl/ 获取

© . All rights reserved.