Logo
blank Skip to main content

Using Libcurl to Download Files in C++ Synchronously, Asynchronously, and with Multiplexing

C++

Applications are useless without their contents: text notes, images, audio and video recordings. But all these files donโ€™t come out of nowhere. Your application must be able to download the data it needs from the internet.

In this article, we overview libcurl โ€“ a popular library for transferring files โ€“ and explain how to use libcurl in C++ solutions. We also describe three methods for downloading multiple files with libcurl in C++ applications โ€“ multiplexing, synchronous, and asynchronous downloading โ€“ and provide detailed code samples for each.

This article will be useful for C++ developers who are getting acquainted with network communication peculiarities and want to implement different file downloading options in their applications.

What is libcurl?

There are many tools that simplify the work of C++ developers. One of them is a popular multiprotocol file transfer library called libcurl.

What is libcurl? Libcurl is part of the Client URL (cURL) project, which consists of two major components: a library with all the functions you might need for implementing file transfer features in your applications and a command-line tool called curl. As stated by its creators, the use cases of curl range from media players and mobile phones to television sets and even cars.

The best thing about this client-side URL transfer library is that itโ€™s free and easy to use. Libcurl supports a wide selection of protocols (including HTTP and HTTPS), certificates, and file uploading and user authentication methods. Libcurl comes with the C API, and each function call to the library is part of this API.

Now that weโ€™ve briefly overviewed the library itself, letโ€™s move to the tasks you can accomplish with its help.

In this article, we focus on three methods for downloading files with libcurl in C++ apps:

  • Synchronous
  • Asynchronous
  • Multiplexing
file downloading methods

To make it easier for you to recreate all the actions described in this article, weโ€™ve used the examples provided by libcurl:

Make sure to download these examples before getting started. Also, note that these examples are provided for a Visual Studio 2015 project that uses the latest versions of the curl-vc140-static-32_64 and openssl-vc140-static-32_64 NuGet packages.

Ready to enhance your application’s capabilities? 

Reach out to us now for expert assistance in C/C++ development and drive efficiency and reliability in your network connections!

Synchronous file downloading

Weโ€™ll start with a description of the synchronous file downloading method. With synchronous downloading, all requests are executed in a strict sequence. While this file transfer method is the easiest, itโ€™s pretty inefficient and canโ€™t be used when you need to transfer multiple files at once.

Letโ€™s start by trying to download sample files from https.c.

1. First of all, we need to initialize global libcurl variables by calling the following function:

C
curl_global_init(CURL_GLOBAL_DEFAULT);

When weโ€™ve finished working with libcurl, we need to uninitialize its global variables by calling the curl_global_cleanup(); function.

Note: Libcurl calls the curl_global_init function at any call to the libcurl method as a fault safety measure. Therefore, in small programs, we can skip calls to curl_global_init and curl_global_cleanup.

We can also create a static variable that will call the curl_global_init(CURL_GLOBAL_DEFAULT); function in the constructor and the curl_global_cleanup(); function in the destructor. To do so, weโ€™ll use the CurlGlobalStateGuard class:

C#
class CurlGlobalStateGuard
{
public:
    CurlGlobalStateGuard() { curl_global_init(CURL_GLOBAL_DEFAULT); }
    ~CurlGlobalStateGuard() { curl_global_cleanup(); }
};
static CurlGlobalStateGuard handle_curl_state;

2. To perform synchronous downloads, we need to use libcurlโ€™s easy interface. We start with creating a curl easy handle for setting connection options and transferring data.

First, we create an easy handle by calling curl_easy_init(). When the target file is successfully downloaded, this handle should be freed by calling the curl_easy_cleanup(CURL*) function. 

At this step, we need to apply a standard unique_ptr smart pointer:

C#
using EasyHandle = std::unique_ptr<CURL, std::function<void(CURL*)>>;
EasyHandle CreateEasyHandle()
{
    auto curl = EasyHandle(curl_easy_init(), curl_easy_cleanup);
    if (!curl)
    {
        throw std::runtime_error("Failed creating CURL easy object");
    }
    return curl;
}

We can use the unique_ptr smart pointer instead of a class definition to keep the CURL pointer for us and call the curl_easy_cleanup function in the destructor.

3. In this example, weโ€™ll download three files. Therefore, we need to create three handles:

C#
std::list<EasyHandle> handles(3);
/* init easy stacks */
try
{
    std::for_each(handles.begin(), handles.end(), [](auto& handle) {handle = CreateEasyHandle(); });
}
catch (const std::exception& ex)
{
    std::cerr << ex.what() << std::endl;
    return -1;
}

To simplify the use of multiple handles for similar actions, we can use a container (such as std::list) and the std::for_each function.

4. At this step, we set options for easy handles. To add links to the files that need to be downloaded, we set the CURLOPT_URL option:

C#
for (auto& handle : handles)
{
    /* set options */
    curl_easy_setopt(handle.get(), CURLOPT_URL, "https://raw.githubusercontent.com/curl/curl/master/docs/examples/https.c");
}

5. Next, we need to deal with SSL connections. To simplify our tutorial, we can tell libcurl not to verify SSL connections by setting the CURLOPT_SSL_VERIFYPEER and CURLOPT_SSL_VERIFYHOST options to 0:

C#
void set_ssl(CURL* curl)
{
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYPEER, 0L);
    curl_easy_setopt(curl, CURLOPT_SSL_VERIFYHOST, 0L);
}
/* ... */
for (auto& handle : handles)
{
    set_ssl(handle.get());
}

Note: Nevertheless, curl provides multiple options for verifying server certificates. One such option is CURLOPT_SSLCERT.

6. By default, curl will print downloaded data to the console, which is useful for testing purposes. To change this behavior, we can redefine the CURLOPT_WRITEFUNCTION option and change it to write to a file:

C
namespace
{
size_t write_to_file(void* contents, size_t size, size_t nmemb, void* userp)
{
    size_t realsize = size * nmemb;
    auto file = reinterpret_cast<std::ofstream*>(userp);
    file->write(reinterpret_cast<const char*>(contents), realsize);
    return realsize;
}
}
 
void save_to_file(CURL* curl)
{
    static std::ofstream file("downloaded_data.txt", std::ios::binary);
    curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, write_to_file);
    curl_easy_setopt(curl, CURLOPT_WRITEDATA, reinterpret_cast<void*>(&file));
}
/* ... */
for (auto& handle : handles)
{
    set_ssl(handle.get());
}

In this example, we open the downloaded_data.txt file in std::fstream and set its address to the CURLOPT_WRITEDATA option. Libcurl will pass this option to the write_to_file function. Then we set the CURLOPT_WRITEFUNCTION option for the write_to_file function to write incoming data to a file.

These options should be applied to all curl easy handles we use.

7. At this point, we can call curl_easy_perform to execute our file download requests.

C
for (auto& handle : handles)
{        
    /* Perform the request, res will get the return code */
    auto res = curl_easy_perform(handle.get());
    /* Check for errors */
    if (res != CURLE_OK)
    {
        std::cerr << "curl_easy_perform() failed:" <<
            curl_easy_strerror(res) << std::endl;
        return -1;
    }
}

The curl_easy_perform function doesnโ€™t return until the request is finished, either successfully or unsuccessfully. Once the request is executed, libcurl will check everything for errors and output the error string to the std::cerr object.

Hereโ€™s the full code for executing synchronous file downloading with libcurl:

C
int download_synchronous(void)
{
    std::list<easyhandle> handles(3);
 
    /* init easy stacks */
    try
    {
        std::for_each(handles.begin(), handles.end(), [](auto& handle) {handle = CreateEasyHandle(); });
    }
    catch (const std::exception& ex)
    {
        std::cerr << ex.what() << std::endl;
        return -1;
    }
 
    for (auto& handle : handles)
    {
        /* set options */
        curl_easy_setopt(handle.get(), CURLOPT_URL, "https://raw.githubusercontent.com/curl/curl/master/docs/examples/https.c");
        set_ssl(handle.get());
        save_to_file(handle.get());
        /* Perform the request, res will get the return code */
        auto res = curl_easy_perform(handle.get());
        /* Check for errors */
        if (res != CURLE_OK)
        {
            std::cerr << "curl_easy_perform() failed:" <<
                curl_easy_strerror(res) << std::endl;
            return -1;
        }
    }    
 
    return 0;
}

Next, we move to a step-by-step description of the asynchronous file downloading method.

Read also

How to Handle Legacy Code: A Detailed Guide Based on a Real-Life Example

Create an effective strategy for source code support and protect your product from inconsistencies and outdated elements. Discover our step-by-step guide to handling legacy code and maintaining a well-functioning structure for your software product!

Learn more

Asynchronous file downloading

In asynchronous file downloading, all requests are executed in an arbitrary order. This method is considered more efficient than synchronous downloading, but itโ€™s also more difficult and less comfortable to use.

For asynchronous downloading, we still need to create curl easy handles and set options for them. So the first seven steps will be the same as for synchronous downloading.

However, asynchronous downloading in libcurl can be achieved through the curl-multi interface. With a multi handle and the curl-multi interface, you can perform several simultaneous transfers in parallel. In this case, every separate transfer will be built around a separate curl easy handle.

Letโ€™s see how it works.

1. First, we create a curl multi handle using the curl_multi_init function. Then we need to use the curl_multi_* function to set options for this handle, add curl easy handles to the multi handle, and perform downloads. Calling the curl_multi_cleanup function will free the curl multi handle.

Similar to with the curl easy handle, we can do all this with help of unique_ptr:

C
using MultiHandle = std::unique_ptr<CURLM, std::function<void(CURLM*)>>;
MultiHandle CreateMultiHandle()
{
    auto curl = MultiHandle(curl_multi_init(), curl_multi_cleanup);
    if (!curl)
    {
        throw std::runtime_error("Failed creating CURL multi object");
    }
    return curl;
}

Once again, weโ€™ve created a guard for the CURLM pointer by calling curl_multi_init(). Upon destruction of the unique_ptr smart pointer, this guard will be passed to the curl_multi_cleanup(CURLM*) function.

2. Using the created guard, we can now create a curl multi handle. Similar to the previous example, we create three curl easy handles for downloading three sample files:

C
std::list<easyhandle> handles(3);
MultiHandle multi_handle;
 
/* init easy and multi stacks */
try
{
    multi_handle = CreateMultiHandle();
    std::for_each(handles.begin(), handles.end(), [](auto& handle){handle = CreateEasyHandle(); });
}
catch (const std::exception& ex)
{
    std::cerr << ex.what() << std::endl;
    return -1;
}

3. We donโ€™t need to set up any other options for the curl multi handler and can now move to adding curl easy handles to our curl multi handle.

C
/* set options */
std::for_each(handles.begin(), handles.end(), [](auto& handle) {
    curl_easy_setopt(handle.get(), CURLOPT_URL, "https://raw.githubusercontent.com/curl/curl/master/docs/examples/multi-double.c");
    set_ssl(handle.get());
    save_to_file(handle.get());
});
 
/* add the individual transfers */
std::for_each(handles.begin(), handles.end(), [&multi_handle](auto& handle) {curl_multi_add_handle(multi_handle.get(), handle.get()); });

4. Next, we perform requests by implementing a sequence of calls to the curl_multi_perform function and a loop of the curl_multi_timeout, curl_multi_fdset, select, and curl_multi_perform functions. The curl_multi_perform function should return a non-zero count of running handles.

In our example, we put this routine into a single dedicated multi_loop function. Letโ€™s see whatโ€™s going on inside this loop.

4.1 The first call is to the curl_multi_perform function. During this call, if thereโ€™s any operation ready on the socket, whether read or write, it will be executed here. Also, this call assigns the count of currently running curl easy handles to the second argument. If this value is equal to 0, it means all operations have finished.

4.2 If the previous value is positive, it means there are still some curl easy handles running and we should wait for the suggested timeout. 

C
int still_running = 0; /* keep number of running handles */
/* we start some action by calling perform right away */
curl_multi_perform(multi_handle, &still_running);
while (still_running) {
    /*...*/
}

To find out the suggested timeout duration, we need to call the curl_multi_timeout function. It assigns to the second argument the value in milliseconds that we should wait before the next call to curl_multi_perform. If this value is 0, it means we should call curl_multi_perform without waiting. If the value is negative, libcurl has no timeout set and we should wait, say, 100 milliseconds.

C
timeval get_timeout(CURLM* multi_handle)
{
    long curl_timeo = -1;
    /* set a suitable timeout to play around with */
    struct timeval timeout;
    timeout.tv_sec = 1;
    timeout.tv_usec = 0;
 
    curl_multi_timeout(multi_handle, &curl_timeo);
    if (curl_timeo >= 0) {
        timeout.tv_sec = curl_timeo / 1000;
        if (timeout.tv_sec > 1)
            timeout.tv_sec = 1;
        else
            timeout.tv_usec = (curl_timeo % 1000) * 1000;
    }
    return timeout;
}
/*...*/
while (still_running) {
    struct timeval timeout = get_timeout(multi_handle);        
    /*...*/
}

4.3 The next step is to wait for actions on sockets. To do so, we need to extract file descriptor information for the fd file from the curl multi handle using the curl_multi_fdset function. As this function only adds file descriptors to input parameters, before calling it, we need to zero out all fd_set variables using FD_ZERO

The maxfd parameter is set to the value of the maximum file descriptor. If this value is -1, we need to wait 100 milliseconds according to the curl_multi_fdset documentation. If this value isnโ€™t equal to -1, we can use the acquired file descriptors in the select function call. It uses maxfd and all fd_sets and timeout values to wait for some actions on the file descriptors. The function call returns the number of sockets that are ready for read/write operations.

C
int wait_if_needed(CURLM* multi_handle, timeval& timeout)
{
    fd_set fdread;
    fd_set fdwrite;
    fd_set fdexcep;
    FD_ZERO(&fdread);
    FD_ZERO(&fdwrite);
    FD_ZERO(&fdexcep);
    int maxfd = -1;
    /* get file descriptors from the transfers */
    auto mc = curl_multi_fdset(multi_handle, &fdread, &fdwrite, &fdexcep, &maxfd);
 
    if (mc != CURLM_OK) {
        std::cerr << "curl_multi_fdset() failed, code " << mc << "." << std::endl;
    }
    /* On success the value of maxfd is guaranteed to be >= -1. We call
           sleep for 100ms, which is the minimum suggested value in the
           curl_multi_fdset() doc. */
    if (maxfd == -1) {
        std::this_thread::sleep_for(std::chrono::milliseconds(100));
    }
 
    int rc = maxfd != -1 ? select(maxfd + 1, &fdread, &fdwrite, &fdexcep, &timeout) : 0;
    return rc;
}
/*...*/
while (still_running) {
    /*...*/
    auto rc = wait_if_needed(multi_handle, timeout);        
    /*...*/
}

4.4 Next, we call the curl_multi_perform function to handle actions on sockets.

C
while (still_running) {
    /*...*/
    auto rc = wait_if_needed(multi_handle, timeout);        
    if (rc >= 0)
    {
        /* timeout or readable/writable sockets */
        curl_multi_perform(multi_handle, &still_running);
    }
}

During execution of this loop, all curl easy handles will receive data and write it to a file. This approach is especially useful when you donโ€™t want to deal with threads.

You can find the code for the whole loop here

5. Finally, after executing all requests, we need to remove curl easy handles from the multi handle:

C
multi_loop(multi_handle.get());
 
std::for_each(handles.begin(), handles.end(), [&multi_handle](auto& handle) {curl_multi_remove_handle(multi_handle.get(), handle.get()); });

Hereโ€™s the full code for asynchronous file downloading:

C
int download_asynchronous(void)
{
    std::list<easyhandle> handles(3);
    MultiHandle multi_handle;
 
    /* init easy and multi stacks */
    try
    {
        multi_handle = CreateMultiHandle();
        std::for_each(handles.begin(), handles.end(), [](auto& handle){handle = CreateEasyHandle(); });
    }
    catch (const std::exception& ex)
    {
        std::cerr << ex.what() << std::endl;
        return -1;
    }
    /* set options */
    std::for_each(handles.begin(), handles.end(), [](auto& handle) {
        curl_easy_setopt(handle.get(), CURLOPT_URL, "https://raw.githubusercontent.com/curl/curl/master/docs/examples/multi-double.c");
        set_ssl(handle.get());
        save_to_file(handle.get());
    });
 
    /* add the individual transfers */
    std::for_each(handles.begin(), handles.end(), [&multi_handle](auto& handle) {curl_multi_add_handle(multi_handle.get(), handle.get()); });
 
    multi_loop(multi_handle.get());
 
    std::for_each(handles.begin(), handles.end(), [&multi_handle](auto& handle) {curl_multi_remove_handle(multi_handle.get(), handle.get()); });
    return 0;
}

Now itโ€™s time to move to the third method: multiplexing.

Read also

How to Build a Mobile Device Management (MDM) System?

Secure your mobile devices and sensitive data with an MDM solution! Get a step-by-step guide on implementing MDM components and rely on the code examples from our experts to enhance your organizationโ€™s cybersecurity!

Learn more
How to Build a Mobile Device Management (MDM) System

Multiplexing

During multiplexing, multiple requests are executed via a single Transmission Control Protocol (TCP) connection. This method was introduced as part of the HTTP/2 protocol.

Multiplexing allows for reusing a single connection to one server for processing multiple HTTP requests. This method can improve the performance of request-heavy applications by eliminating the need to close and reopen server connections.

To use multiplexing in libcurl, we need to configure it to use the HTTP/2 protocol.

1. libcurl provides support for request multiplexing via the curl-multi interface. We can reuse the asynchronous example and add this code between setting options for curl easy handles and add curl easy handles to the curl multi handle (step 3 in the previous section):

C
/*...*/
for(auto& handle : handles)
{
    /* HTTP/2 please */
    curl_easy_setopt(handle.get(), CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
    /* wait for pipe connection to confirm */
    curl_easy_setopt(handle.get(), CURLOPT_PIPEWAIT, 1L);
}
curl_multi_setopt(multi_handle.get(), CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);

The CURLOPT_HTTP_VERSION option with the CURL_HTTP_VERSION_2_0 value asks libcurl to use the HTTP/2 protocol. If a server doesnโ€™t support HTTP/2, the protocol is downgraded to HTTP/1.1.

The CURLOPT_PIPEWAIT option in value 1 asks libcurl to check if thereโ€™s any connection that enables pipelining or multiplexing. If detected, such a connection will be used instead of creating a new connection.

The CURLMOPT_PIPELINING option in the CURLPIPE_MULTIPLEX value will try to execute transfers over existing connections.

Hereโ€™s a full example of a multiplexing method implementation in libcurl:

C
/*
 * Download many transfers over HTTP/2 using the same connection!
 */
int download_multiplexing(void)
{
    std::list<easyhandle> handles(3);
    MultiHandle multi_handle;
 
    /* init easy and multi stacks */
    try
    {
        multi_handle = CreateMultiHandle();
        for(auto& handle : handles)
        {
            handle = CreateEasyHandle();
            /* HTTP/2 please */
            curl_easy_setopt(handle.get(), CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_2_0);
            /* wait for pipe connection to be confirmed */
            curl_easy_setopt(handle.get(), CURLOPT_PIPEWAIT, 1L);
        }
    }
    catch (const std::exception& ex)
    {
        std::cerr << ex.what() << std::endl;
        return -1;
    }
 
    for (auto& handle : handles)
    {
        curl_easy_setopt(handle.get(), CURLOPT_URL, "https://raw.githubusercontent.com/curl/curl/master/docs/examples/http2-download.c");
        set_ssl(handle.get());
        save_to_file(handle.get());
        /* add the individual transfers */
        curl_multi_add_handle(multi_handle.get(), handle.get());
    }
 
    curl_multi_setopt(multi_handle.get(), CURLMOPT_PIPELINING, CURLPIPE_MULTIPLEX);
 
    multi_loop(multi_handle.get());
     
    std::for_each(handles.begin(), handles.end(), [&multi_handle](auto& handle) {curl_multi_remove_handle(multi_handle.get(), handle.get()); });
    return 0;
}

Which method should you choose?

Letโ€™s summarize the pros and cons of each libcurl file downloading method:

 file downloading methods comparison

You can find the full source code of all examples from this article on our GitHub page.

Related project

Improving a Windows Audio Driver to Obtain a WHQL Release Signature

Learn how Apriorit streamlined the WHQL release signature acquisition process for a client’s audio driver, ensuring compliance and stability. Explore how our expertise in driver development led to smoother performance and increased user satisfaction.

Project details

Conclusion

Downloading files is one of the basic activities any application should be able to perform. Developers can enable a C++ solution to download a file with curl, a popular file transfer library.

The three most widely used file downloading methods are multiplexing, synchronous, and asynchronous. Each has its advantages and drawbacks and is best used in specific cases.

At Apriorit, we have a team of passionate Linux, macOS, and C++ developers who create performant, stable, and secure applications of any complexity. Get in touch with us and weโ€™ll start discussing your next project right away.

Looking for an expert team of C/C++ developers? 

Let’s chat about how we can empower your project’s success together!

Have a question?

Ask our expert!

Tell us about
your project

...And our team will:

  • Process your request within 1-2 business days.
  • Get back to you with an offer based on your project's scope and requirements.
  • Set a call to discuss your future project in detail and finalize the offer.
  • Sign a contract with you to start working on your project.

Do not have any specific task for us in mind but our skills seem interesting? Get a quick Apriorit intro to better understand our team capabilities.