Share via


Asynchronous Completion in WinHTTP

This topic discusses creating a sample Microsoft Windows HTTP Services (WinHTTP) application that enables asynchronous completions.

Warning  WinHTTP is not reentrant except during asynchronous completion callback. That is, while a thread has a call pending to one of the WinHTTP functions such as WinHttpSendRequest, WinHttpReceiveResponse, WinHttpQueryDataAvailable, WinHttpReadData, or WinHttpWriteData, it must never call WinHTTP a second time until the first call has completed. One scenario under which a second call could occur is as follows: If an application queues an Asynchronous Procedure Call (APC) to the thread that is calling into WinHTTP, and if WinHTTP performs an alertable wait internally, the APC will get a chance to run. If the APC routine happens also to call WinHTTP, it reenters the WinHTTP API, and the internal state of WinHTTP can be corrupted.

The sample presented below is intended to let a user download two resources simultaneously using the HTTP protocol while showing status information in a listbox. The topics covered in this section include creating a callback function, using WinHTTP functions to connect to a remote server, and requesting an HTML document.

  • Setup and Requirements
  • Source Code: the Starting and End Points
  • Opening and Changing the Project
  • Overview of the Application
  • Adding the New Code
    • Initialize WinHTTP
    • Declare Context Structures
    • Write a Status Callback Function
    • Add a Helper Function to Request a Resource
    • Add a Helper Function to Retrieve Response Headers
    • Add a Helper Function to Clean Up Contexts
    • Add Helper Functions to Retrieve Response Contents
  • Trying It Out
  • Additional Error-Handling Considerations

Setup and Requirements

To build the sample code presented here, be sure that you have an up-to-date version of the Windows SDK installed, including headers and libraries, and make sure that your include and library paths are set correctly to pick up the current files. The WinHTTP header file is "Winhttp.h", and the library file is "Winhttp.lib". If using the older SDK header files that come with Visual Studio 6.0, the project will not build correctly (for example, more recent type definitions such as 'DWORD_PTR' will be missing).

You should have an understanding of C/C++ and some familiarity with WinHTTP C/C++ API Reference and HINTERNET handles.

If you use a proxy server to access remote HTTP servers, use the proxy configuration tool. For more information, see ProxyCfg.exe, a Proxy Configuration Tool.

Source Code: the Starting and End Points

Two versions of source code for the sample are provided, both named "AsyncDemo.cpp":

  • The first, located in the "asyncdemotutorial" folder, is the starting point for this walk-through, and implements a basic application with a dialog box. By adding code as described below, you can expand this sample into a more functional WinHTTP application.
  • The second version, contained in the "asyncdemocomplete" folder, is more or less where you should end up after making the changes described below.

Note  When using the source code provided in this example and in the samples for your own applications, you do so at your own risk. The samples are not intended to be production-quality code and might have to be modified to meet specific requirements.

Opening and Changing the Project

The following procedure describes how to change the "AsyncDemo.cpp" file contained in the "asyncdemotutorial" folder.

Aa383138.wedge(en-us,VS.85).gifTo open and change the project

  1. Open Microsoft Visual C++.
  2. From the File menu, click Open Workspace.
  3. Locate "AsyncDemo.dsw" in the "asyncdemotutorial" folder and open the file.
  4. In the workspace window, docked on the left side of the Visual C++ window by default, click the FileView tab.
  5. Expand the AsyncDemo files tree. Locate the file "AsyncDemo.cpp" and double click to edit this file.

This file contains the code necessary to open the dialog box, in the figure below, with a functioning Exit button.

 

AsyncDemo dialog box

AsyncDemo dialog box

 

You may want to examine the starting source code in detail, and then build and run the project, before going on to add new structures and functions as described below.

Overview of the Application

The completed application will use a WinHTTP callback function to enable asynchronous completions, so that a slow connection to one HTTP server does not affect the retrieval of resources from another. The operations that the application must perform are as follows:

  • Initialize WinHTTP using WinHttpOpen.  It is recommended that this is performed in "WinMain" where the dialog box is also initialized.
  • Choose an HTTP server.  A recommended place to implement selection of an HTTP server is in the dialog callback function, called when the user clicks on the dialog Download button.
  • Request a document and wait for a response.  The dialog callback function is also the right place to request a document from that server, using WinHttpConnect, WinHttpOpenRequest, and WinHttpSendRequest. In asynchronous mode, however, WinHTTP functions do not necessarily wait until their operations are complete before returning. Instead, status data for an operation is retrieved in a WinHTTP callback function called when the status of the HTTP connection changes. Add a helper function, for the callback to retrieve response headers, using WinHttpQueryHeaders and another to retrieve the document itself using WinHttpQueryDataAvailable and WinHttpReadData.
  • Display the headers accompanying that response.
  • Display the requested document.

The following figure outlines the overall structure of the final program.

 

A Plan of AsyncDemo

A Plan of AsyncDemo

 

Adding the New Code

After each of the following steps, you should be able to compile and run the application, provided that the steps are completed in order:

  • Initialize WinHTTP
  • Declare Context Structures
  • Write a Status Callback Function
  • Add a Helper Function to Request a Resource
  • Add a Helper Function to Retrieve Response Headers
  • Add a Helper Function to Clean Up Contexts
  • Add Helper Functions to Retrieve Response Contents

Initialize WinHTTP

Use the WinHttpOpen function to initialize WinHTTP and generate a session handle. The first step is to declare a global variable to contain the session handle. Do this in the section titled "Global Variables" near the top of "AsyncDemo.cpp" as follows:

// Session handle.
HINTERNET hSession;

The next step is to modify the "WinMain" function to call WinHttpOpen, and then display the dialog box only if initialization succeeds. To clean up, WinHttpCloseHandle should then be called after the DialogBox function returns, to release the HINTERNET handle.

The first parameter of WinHttpOpen is a string specifying the name of the user agent. Use the string, "Asynchronous WinHTTP Demo/1.0" as the name of the agent. WinHttpOpen also requires a flag that specifies proxy settings in the dwAccessType parameter. If you do not access the Internet through a proxy or previously configured your proxy settings with the proxy configuration tool, you can use the access information specified in the registry with the WINHTTP_ACCESS_TYPE_DEFAULT_PROXY flag.

Next, set the pwszProxyName parameter to WINHTTP_NO_PROXY_NAME and the pwszProxyBypass parameter to WINHTTP_NO_PROXY_BYPASS to specify the use of the proxy settings created by the proxy configuration tool.

If asynchronous mode is used, WinHTTP creates a number of threads for asynchronous selection and timer pooling. WinHTTP creates one thread per CPU, plus one extra (CPU + 1). If DNS timeout is specified using NAME_RESOLUTION_TIMEOUT, there is an overhead of one thread per request.

Set the last parameter, dwFlags, to WINHTTP_FLAG_ASYNC to enable asynchronous services.

The resulting "WinMain" function should look something like this:

int WINAPI WinMain( HINSTANCE hThisInst, HINSTANCE hPrevInst,
                    LPSTR lpszArgs, int nWinMode)
{
    UNREFERENCED_PARAMETER(hPrevInst);
    UNREFERENCED_PARAMETER(lpszArgs);
    UNREFERENCED_PARAMETER(nWinMode);
    
    // Create the session handle using the default settings.
    hSession = WinHttpOpen( L"Asynchronous WinHTTP Demo/1.0", 
                            WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                            WINHTTP_NO_PROXY_NAME,
                            WINHTTP_NO_PROXY_BYPASS,
                            WINHTTP_FLAG_ASYNC);

    // Check to see if the session handle was successfully created.
    if(hSession != NULL)
    {   
        InitializeCriticalSection(&g_CallBackCritSec);

        // Show the dialog box.
        DialogBox( hThisInst, MAKEINTRESOURCE(IDD_DIALOG1), 
                   HWND_DESKTOP, (DLGPROC)AsyncDialog );

        DeleteCriticalSection(&g_CallBackCritSec);

        // Close the session handle.
        WinHttpCloseHandle(hSession);

    }

    return 0;
}

Declare Context Structures

For asynchronous requests, the WinHTTP functions require the application to provide a nonzero context value. The context value provides a way for your status callback function to identify the request that caused the status callback, and you can also use it to provide other information used in processing the callback notification.

The context value can be any variable that can be cast as a DWORD_PTR value. One possibility is to pass the address of a structure that contains the resources required by your application.

Such a structure, named REQUEST_CONTEXT, is already defined in the "Global Variables" section of the application. Declare two static instances of this structure just below its typedef, one for each of the two HTTP servers that the application will connect to. At the same time, add a declaration for a CRITICAL_SECTION variable that will be used by one of the helper functions.

// Context value structure.
typedef struct {
    HWND        hWindow;        // Handle for the dialog box
    HINTERNET   hConnect;       // Connection handle
    HINTERNET   hRequest;       // Resource request handle
    int         nURL;           // ID of the URL edit box
    int         nHeader;        // ID of the header output box
    int         nResource;      // ID of the resource output box
    DWORD       dwSize;         // Size of the latest data block
    DWORD       dwTotalSize;    // Size of the total data
    LPSTR       lpBuffer;       // Buffer for storing read data
    WCHAR       szMemo[256];    // String providing state information
} REQUEST_CONTEXT;


// Two Instances of the context value structure.
static REQUEST_CONTEXT rcContext1, rcContext2;

CRITICAL_SECTION g_CallBackCritSec;

Initialize the two context structures with appropriate values in the dialog box callback function when the dialog itself is being initialized:

    case WM_INITDIALOG:
        // Set the default web sites.
        SetDlgItemText(hX, IDC_URL1, L"https://www.microsoft.com");
        SetDlgItemText(hX, IDC_URL2, L"https://www.msn.com");

        // Initialize the first context value.
        rcContext1.hWindow = hX;
        rcContext1.nURL = IDC_URL1;
        rcContext1.nHeader = IDC_HEADER1;
        rcContext1.nResource = IDC_RESOURCE1;
        rcContext1.hConnect = 0;
        rcContext1.hRequest = 0;
        rcContext1.lpBuffer = NULL;
        rcContext1.szMemo[0] = 0;

        // Initialize the second context value.
        rcContext2.hWindow = hX;
        rcContext2.nURL = IDC_URL2;
        rcContext2.nHeader = IDC_HEADER2;
        rcContext2.nResource = IDC_RESOURCE2;
        rcContext2.hConnect = 0;
        rcContext2.hRequest = 0;
        rcContext2.lpBuffer = NULL;
        rcContext2.szMemo[0] = 0;

        return TRUE;

Write a Status Callback Function

When using the WinHTTP application programming interface (API) asynchronously, you must write a status callback function to obtain completion information about the functions that you call. Every time the status changes on an HINTERNET handle, the corresponding callback is called with a notification flag that identifies the type of event that just occurred. Your application can then retrieve information about the event and take whatever action is appropriate.

Note  Status callback functions must be threadsafe.

The starting code in this sample already contains a status callback function.

When the WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE notification is received, a message is assembled to inform the user of the current status. The szMemo member of the context structure contains the name of the last WinHTTP function called and an identifying number to indicate which of two resource requests caused the callback. The dwStatusInformationLength parameter of the INTERNET_STATUS_CALLBACK_PROTOTYPE callback function contains information that varies depending on the type of notification.

In the starting version of the callback, all notifications are handled just by assembling and displaying a message. You will modify some notifications to call the helper functions that retrieve response headers and the contents.

Add a Helper Function to Request a Resource

A helper function named SendRequest actually prepares and sends HTTP requests to servers.

The first step in this process is to use WinHttpCrackUrl to parse the URL supplied by the user into component parts in a URL_COMPONENTS structure. Next, SendRequest calls WinHttpConnect to open an HTTP session with the specified host server. If this succeeds, it calls WinHttpOpenRequest to open an appropriate "GET" request handle, and WinHttpSetStatusCallback to set your callback function for that handle. Finally, it calls WinHttpSendRequest to send the GET request to the server.

To implement the SendRequest function, add code resembling the following to the "Additional Functions" section of the "AsyncDemo.cpp" file:

BOOL SendRequest(REQUEST_CONTEXT *cpContext, LPWSTR szURL)
{
    WCHAR szHost[256];
    DWORD dwOpenRequestFlag = 0;
    URL_COMPONENTS urlComp;
    BOOL fRet = FALSE;

    // Initialize URL_COMPONENTS structure.
    ZeroMemory(&urlComp, sizeof(urlComp));
    urlComp.dwStructSize = sizeof(urlComp);

    // Use allocated buffer to store the Host Name.
    urlComp.lpszHostName        = szHost;
    urlComp.dwHostNameLength    = sizeof(szHost) / sizeof(szHost[0]);

    // Set non-zero lengths to obtain pointer a to the URL Path.
    // NOTE: If we threat this pointer as a null-terminated string,
    //       it gives us access to additional information as well. 
    urlComp.dwUrlPathLength = (DWORD)-1;

    // Crack HTTP scheme.
    urlComp.dwSchemeLength = (DWORD)-1;

    // Set the szMemo string.
    swprintf_s( cpContext->szMemo, L"WinHttpCrackURL (%d)", cpContext->nURL);

    // Crack the URL.
    if (!WinHttpCrackUrl(szURL, 0, 0, &urlComp))
    {
        goto cleanup;
    }

    // Set the szMemo string.
    swprintf_s( cpContext->szMemo, L"WinHttpConnect (%d)", cpContext->nURL);

    // Open an HTTP session.
    cpContext->hConnect = WinHttpConnect(hSession, szHost, 
                                    urlComp.nPort, 0);
    if (NULL == cpContext->hConnect)
    {
        goto cleanup;
    }
    
    // Prepare OpenRequest flag
    dwOpenRequestFlag = (INTERNET_SCHEME_HTTPS == urlComp.nScheme) ?
                            WINHTTP_FLAG_SECURE : 0;

    // Set the szMemo string.
    swprintf_s( cpContext->szMemo, L"WinHttpOpenRequest (%d)", cpContext->nURL);

    // Open a "GET" request.
    cpContext->hRequest = WinHttpOpenRequest(cpContext->hConnect, 
                                        L"GET", urlComp.lpszUrlPath,
                                        NULL, WINHTTP_NO_REFERER, 
                                        WINHTTP_DEFAULT_ACCEPT_TYPES,
                                        dwOpenRequestFlag);

    if (cpContext->hRequest == 0)
    {
        goto cleanup;
    }

    // Set the szMemo string.
    swprintf_s( cpContext->szMemo, L"WinHttpSetStatusCallback (%d)", cpContext->nURL);

    // Install the status callback function.
    WINHTTP_STATUS_CALLBACK pCallback = WinHttpSetStatusCallback(cpContext->hRequest,
                            (WINHTTP_STATUS_CALLBACK)AsyncCallback,
                            WINHTTP_CALLBACK_FLAG_ALL_COMPLETIONS | 
                            WINHTTP_CALLBACK_FLAG_REDIRECT,    
                            NULL);

    // note: On success WinHttpSetStatusCallback returns the previously defined callback function.
    // Here it should be NULL
    if (pCallback != NULL)
    {
        goto cleanup;
    }

    // Set the szMemo string.
    swprintf_s( cpContext->szMemo, L"WinHttpSendRequest (%d)", cpContext->nURL);

    // Send the request.
    if (!WinHttpSendRequest(cpContext->hRequest, 
                        WINHTTP_NO_ADDITIONAL_HEADERS, 0, 
                        WINHTTP_NO_REQUEST_DATA, 0, 0, 
                        (DWORD_PTR)cpContext))
    {
        goto cleanup;
    }

    fRet = TRUE;
 
cleanup:

    if (fRet == FALSE)
    {
        WCHAR szError[256];

        // Set the error message.
        swprintf_s(szError, L"%s failed with error %d", cpContext->szMemo, GetLastError());

        // Cleanup handles.
        Cleanup(cpContext);
                    
        // Display the error message.
        SetDlgItemText(cpContext->hWindow, cpContext->nResource, szError);

    }
                
    return fRet;
}

The SendRequest function should be invoked in the dialog callback, after the Download button has been pressed. Add code such as the following, so that the IDC_DOWNLOAD case in the dialog callback looks like this:

            case IDC_DOWNLOAD:
                WCHAR szURL[256];

                // Disable the download button.
                EnableWindow( GetDlgItem(hX, IDC_DOWNLOAD), 0);

                // Reset the edit boxes.
                SetDlgItemText( hX, IDC_HEADER1, NULL );
                SetDlgItemText( hX, IDC_HEADER2, NULL );
                SetDlgItemText( hX, IDC_RESOURCE1, NULL );
                SetDlgItemText( hX, IDC_RESOURCE2, NULL );

                // Obtain the URLs from the dialog box and send the requests.
                GetDlgItemText( hX, IDC_URL1, szURL, 256);
                BOOL fRequest1 = SendRequest(&rcContext1, szURL);

                GetDlgItemText( hX, IDC_URL2, szURL, 256);
                BOOL fRequest2 = SendRequest(&rcContext2, szURL);

                if (!fRequest1 && !fRequest2)
                {
                    // Enable the download button if both requests are failing.
                    EnableWindow( GetDlgItem(hX, IDC_DOWNLOAD), TRUE);
                }

                return (fRequest1 && fRequest2);

After the request has successfully been sent, your callback function is called with a WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE completion. At that point, the callback function should call WinHttpReceiveResponse to begin receiving the response.

Modify the WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE case in the callback to look like the following:

        case WINHTTP_CALLBACK_STATUS_SENDREQUEST_COMPLETE:
            swprintf_s(szBuffer,L"%s: SENDREQUEST_COMPLETE (%d)", 
                cpContext->szMemo, dwStatusInformationLength);

            // Prepare the request handle to receive a response.
            if (WinHttpReceiveResponse( cpContext->hRequest, NULL) == FALSE)
            {
                Cleanup(cpContext);
            }
            break;

Add a Helper Function to Retrieve Response Headers

When a response comes back from an HTTP server and WinHTTP is operating asynchronously, your callback function receives a WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE completion to indicate that response headers are available. At that point, your callback needs to retrieve the headers and then start downloading the content.

Modify the WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE case in the status callback to look like this:

        case WINHTTP_CALLBACK_STATUS_HEADERS_AVAILABLE:
            swprintf_s(szBuffer,L"%s: HEADERS_AVAILABLE (%d)", 
                cpContext->szMemo, dwStatusInformationLength);
            Header( cpContext);            

            // Initialize the buffer sizes.
            cpContext->dwSize = 0;
            cpContext->dwTotalSize = 0;

            // Begin downloading the resource.
            if (QueryData( cpContext ) == FALSE)
            {
                Cleanup(cpContext);
            }
            break;

Next, add a function called Header to retrieve and display the response headers.

To retrieve the headers, Header uses WinHttpQueryHeaders, which takes six parameters. The first parameter, hRequest, is the request handle created with WinHttpOpenRequest. The second parameter, dwInfoLevel, specifies the headers you wish to retrieve along with formatting information. To retrieve all of the response headers, pass in the value, WINHTTP_QUERY_RAW_HEADERS_CRLF. The third parameter, pwszName, can be set to WINHTTP_HEADER_NAME_BY_INDEX because you are not requesting a particular header by name. The fourth parameter, lpBuffer, is the address of a buffer to hold the headers that are retrieved. If this value is NULL, WinHttpQueryHeaders returns an error and sets the fifth parameter, lpdwBufferLength, to the buffer size, in bytes, required to hold the headers. You can take advantage of this functionality by calling WinHttpQueryHeaders twice, once to find the size of buffer needed and once to actually retrieve the headers. Set the last parameter, lpdwIndex, to WINHTTP_NO_HEADER_INDEX so that all instances of identically named headers are obtained.

To implement the Header helper function, add code resembling the following to the "Additional Functions" section of the "AsyncDemo.cpp" file:

BOOL Header(REQUEST_CONTEXT *cpContext) 
{
    DWORD dwSize=0;
    LPVOID lpOutBuffer = NULL;

    // Set the state memo.
    swprintf_s(cpContext->szMemo, L"WinHttpQueryHeaders (%d)", cpContext->nURL);

    // Use HttpQueryInfo to obtain the size of the buffer.
    if (!WinHttpQueryHeaders( cpContext->hRequest, 
                              WINHTTP_QUERY_RAW_HEADERS_CRLF,
                              WINHTTP_HEADER_NAME_BY_INDEX, NULL, &dwSize, WINHTTP_NO_HEADER_INDEX))
    {
        // An ERROR_INSUFFICIENT_BUFFER is expected because you
        // are looking for the size of the headers.  If any other
        // error is encountered, display error information.
        DWORD dwErr = GetLastError();
        if (dwErr != ERROR_INSUFFICIENT_BUFFER)
        {
            WCHAR  szError[256];
            swprintf_s( szError, L"%s: Error %d encountered.",
                      cpContext->szMemo, dwErr);
            SetDlgItemText( cpContext->hWindow, cpContext->nResource, szError);
            return FALSE;
        }
    }
    
    // Allocate memory for the buffer.
    lpOutBuffer = new WCHAR[dwSize];

    // Use HttpQueryInfo to obtain the header buffer.
    if(WinHttpQueryHeaders( cpContext->hRequest, 
                            WINHTTP_QUERY_RAW_HEADERS_CRLF,
                            WINHTTP_HEADER_NAME_BY_INDEX, lpOutBuffer, &dwSize, WINHTTP_NO_HEADER_INDEX))
        SetDlgItemText( cpContext->hWindow, cpContext->nHeader, 
                        (LPWSTR)lpOutBuffer);

    // Free the allocated memory.
    delete [] lpOutBuffer;

    return TRUE;
}

Add a Helper Function to Clean Up Contexts

In case of errors communicating with an HTTP server represented by a context structure, the application may wish to terminate the connection and clean up the context structure. Another helper function, Cleanup, takes care of closing down a context.

Cleanup starts by setting the status memo in the context structure to "Closed". Then, if the request handle is still valid, Cleanup sets the associated callback function pointer to NULL, closes the request handle, and sets its value to NULL in the context structure. Next, if the connection handle is still valid, Cleanup closes the handle and sets its value to NULL in the context structure. Cleanup then frees the context's data buffer, if any, and sets its pointer to NULL. Finally, Cleanup checks to see whether both contexts have now been closed, and if so, re-enables the Download button in the dialog box. Because one of the two contexts is not the one currently being closed, Cleanup uses a critical section to ensure that its access to the contexts and change to the dialog are protected.

To implement the Cleanup function, add code resembling the following to the "Additional Functions" section of the "AsyncDemo.cpp" file:

void Cleanup (REQUEST_CONTEXT *cpContext)
{
    // Set the memo to indicate a closed handle.
    swprintf_s(cpContext->szMemo, L"Closed");

    if (cpContext->hRequest)
    {
        WinHttpSetStatusCallback(cpContext->hRequest, 
                NULL, 
                NULL, 
                NULL);

        WinHttpCloseHandle(cpContext->hRequest);
        cpContext->hRequest = NULL;
    }

    if (cpContext->hConnect)
    {
        WinHttpCloseHandle(cpContext->hConnect);
        cpContext->hConnect = NULL;
    }

    delete [] cpContext->lpBuffer;
    cpContext->lpBuffer = NULL;

    // note: this function can be called concurrently by differnet threads, therefore any global data
    // reference needs to be protected

    EnterCriticalSection(&g_CallBackCritSec);

    // If both handles are closed, re-enable the download button.
    if ((wcsncmp( rcContext1.szMemo, L"Closed",6)==0) &&
        (wcsncmp( rcContext2.szMemo, L"Closed",6)==0))
    {
        EnableWindow( GetDlgItem(cpContext->hWindow, IDC_DOWNLOAD),1);
    }

    LeaveCriticalSection(&g_CallBackCritSec);

}

Add Helper Functions to Retrieve Response Contents

The AsyncDemo application also retrieves the contents of requested resources and displays them in an edit box. Whether WinHTTP is operating synchronously or asynchronously, retrieving document contents can be accomplished in a simple loop: as long as a call to WinHttpQueryDataAvailable indicates that data is available to be read, call WinHttpReadData to read the data, and repeat.

This control flow is illustrated in the following diagram:

 

Retrieving a Resource

Retrieving a Resource

 

Keep in mind that when WinHTTP is operating asynchronously, WinHttpQueryDataAvailable and WinHttpReadData can complete either synchronously or asynchronously each time they are called.

When WinHttpReadData is called, for example, the callback function is always called with the WINHTTP_CALLBACK_STATUS_READ_COMPLETE completion when the read is done. If the read cannot happen quickly, however, WinHttpReadData returns immediately and the read continues asynchronously on a separate thread until it is finished.

If, on the other hand, the read happens quickly, WinHttpReadData does not return until both the read and its associated callback call are complete. It is important to realize that even in asynchronous mode, the function may complete synchronously, and that when it does, it blocks not only on the read operation but also on the associated callback call.

The following sections walk you through the code needed to implement the retrieval of content illustrated in the diagram above:

  • Query for Data
  • Check How Much Data Remains to be Read
  • Read Data
  • Free Temporary Buffers

Query for Data

The "Query" action in the diagram above is performed by a helper function named QueryData. This function begins by setting szMemo to "WinHttpQueryDataAvailable" so that you can determine in the callback which function was called last. Then, QueryData calls WinHttpQueryDataAvailable with the request handle as the first parameter, hRequest, and NULL as the second parameter, lpdwNumberOfBytesAvailable, since this is to be an asynchronous call.

If a synchronous error occurs, WinHttpQueryDataAvailable returns FALSE, in which case QueryData displays the corresponding error message and returns FALSE as well. Otherwise, the operation has begun, and is either continuing asynchronously or has already completed.

Place code like the following in the "Additional Functions" section of the "AsyncDemo.cpp" file:

BOOL QueryData(REQUEST_CONTEXT *cpContext)
{
    // Set the state memo.
    swprintf_s( cpContext->szMemo, L"WinHttpQueryDataAvailable (%d)", 
              cpContext->nURL);
        
    // Chech for available data.
    if (WinHttpQueryDataAvailable(cpContext->hRequest, NULL) == FALSE)
    {
        // If a synchronous error occured, display the error.  Otherwise
        // the query is successful or asynchronous.
        DWORD dwErr = GetLastError();
        WCHAR  szError[256];
        swprintf_s( szError, L"%s: Error %d encountered.",
                  cpContext->szMemo, dwErr);
        SetDlgItemText( cpContext->hWindow, cpContext->nResource, szError);
        return FALSE;
    }
    return TRUE;
}

Check How Much Data Remains to be Read

The call to WinHttpQueryDataAvailable in QueryData generates a status callback with a WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE completion in the dwInternetStatus parameter. By checking the value pointed to by the lpvStatusInformation parameter, the callback can determine how much data is left to be read, and if there is no remaining data, can proceed to display all the data that has been read.

To implement this behavior, place code in the WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE case of the status callback function in "AsyncDemo.cpp" so that it looks like this:

        case WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE:
            swprintf_s(szBuffer,L"%s: DATA_AVAILABLE (%d)", 
                cpContext->szMemo, dwStatusInformationLength);

            cpContext->dwSize = *((LPDWORD)lpvStatusInformation);

            // If there is no data, the process is complete.
            if (cpContext->dwSize == 0)
            {
                // All of the data has been read.  Display the data.
                if (cpContext->dwTotalSize)
                {
                    // Convert the final context buffer to wide characters,
                    // and display.
                    LPWSTR lpWideBuffer = new WCHAR[cpContext->dwTotalSize + 1];
                    MultiByteToWideChar( CP_ACP, MB_PRECOMPOSED, 
                                         cpContext->lpBuffer, 
                                         cpContext->dwTotalSize, 
                                         lpWideBuffer, 
                                         cpContext->dwTotalSize);
                    lpWideBuffer[cpContext->dwTotalSize] = 0;
                    /* note: in the case of binary data, only data upto the first null will be displayed */
                    SetDlgItemText( cpContext->hWindow, cpContext->nResource,
                                    lpWideBuffer);

                    // Delete the remaining data buffers.
                    delete [] lpWideBuffer;
                    delete [] cpContext->lpBuffer;
                    cpContext->lpBuffer = NULL;
                }

                // Close the request and connect handles for this context.
                Cleanup(cpContext);

            }
            else
                // Otherwise, read the next block of data.
                if (ReadData( cpContext ) == FALSE)
                {
                    Cleanup(cpContext);
                }
            break;

Read Data

If the WINHTTP_CALLBACK_STATUS_DATA_AVAILABLE completion code above determines that there is data to be read, it calls a helper function named ReadData. This function begins by creating a temporary buffer to store the data as it is retrieved, lpOutBuffer. It then sets the state memo to "WinHttpReadData" and calls WinHttpReadData. If a synchronous error occurs, the helper function displays an appropriate error message, frees the temporary buffer that it allocated, and returns FALSE.

Once the asynchronous read operation is successfully initiated, the memory allocated for the temporary read buffers must not be released until the operation completes. For efficiency, WinHTTP does not create internal buffers to store data as it is retrieved, but instead uses the buffer provided in the second parameter of WinHttpReadData. After using the data that has been read, you must delete each of these temporary buffers to free the memory allocated to it. If the read operation is asynchronous, however, do not delete the buffer immediately after calling WinHttpReadData. This causes WinHTTP to try to write data to unallocated memory, and an exception is raised. The correct time to delete the temporary buffer is in the status callback function after the WINHTTP_CALLBACK_STATUS_READ_COMPLETE completion is received.

The ReadData helper function should be implemented with code that resembles the following, placed in the "Additional Functions" section of the "AsyncDemo.cpp" file:

BOOL ReadData(REQUEST_CONTEXT *cpContext)
{
    LPSTR lpOutBuffer = new char[cpContext->dwSize+1];
    ZeroMemory(lpOutBuffer, cpContext->dwSize+1);

    // Set the state memo.
    swprintf_s( cpContext->szMemo, L"WinHttpReadData (%d)", 
              cpContext->nURL);

    // Read the available data.
    if (WinHttpReadData( cpContext->hRequest, (LPVOID)lpOutBuffer, 
                          cpContext->dwSize, NULL) == FALSE)
    {
        // If a synchronous error occurred, display the error.  Otherwise
        // the read is successful or asynchronous.
        DWORD dwErr = GetLastError();
        WCHAR  szError[256];
        swprintf_s( szError, L"%s: Error %d encountered.",
                  cpContext->szMemo, dwErr);
        SetDlgItemText( cpContext->hWindow, cpContext->nResource, szError);
        delete [] lpOutBuffer;
        return FALSE;
    }

    return TRUE;
}

Free Temporary Buffers

After a block of data has been read successfully, the callback function is called again with the WINHTTP_CALLBACK_STATUS_READ_COMPLETE completion. At this point, data in the most recent temporary buffer needs to be added at the end of the document buffer and the temporary folder be deleted (this is the "Copy and delete buffers" action in the diagram above). The read process is then continued by calling QueryData again. When no more data is available, the context is closed, terminating the connection.

The first step in implementing this behavior is to modify the WINHTTP_CALLBACK_STATUS_READ_COMPLETE completion case of the callback function so that it looks like the following:

        case WINHTTP_CALLBACK_STATUS_READ_COMPLETE:
            swprintf_s(szBuffer,L"%s: READ_COMPLETE (%d)", 
                cpContext->szMemo, dwStatusInformationLength);

            // Copy the data and delete the buffers.

            if (dwStatusInformationLength != 0)
            {
                TransferAndDeleteBuffers(cpContext, (LPSTR) lpvStatusInformation, dwStatusInformationLength);

                // Check for more data.
                if (QueryData(cpContext) == FALSE)
                {
                    Cleanup(cpContext);
                }
            }
            break;

Next, add a helper function named TransferAndDeleteBuffers to copy retrieved data into a large buffer that holds the entire document and delete the temporary buffer into which it was downloaded.

TransferAndDeleteBuffers first checks to see whether the large document buffer exists yet. The first time WinHttpReadData is called, the buffer pointer in the context structure is simply set to point the temporary buffer, which becomes the document buffer until the next data block is read.

At each subsequent call, however, a new document buffer is allocated, large enough to hold both the contents of the existing document buffer and the contents of the new temporary buffer. The contents of both these buffers are then copied into the newly allocated one, and the old document buffer and the temporary buffer are deleted. Finally, the dwTotalSize member of the context structure is set to the new size of the document buffer.

The TransferAndDeleteBuffers helper function should be implemented with code that resembles the following, placed in the "Additional Functions" section of the "AsyncDemo.cpp" file:

void TransferAndDeleteBuffers(REQUEST_CONTEXT *cpContext, LPSTR lpReadBuffer, DWORD dwBytesRead)
{
    cpContext->dwSize = dwBytesRead;

    if(!cpContext->lpBuffer)
    {
        // If there is no context buffer, start one with the read data.
        cpContext->lpBuffer = lpReadBuffer;
    }
    else
    {
        // Store the previous buffer, and create a new one big
        // enough to hold the old data and the new data.
        LPSTR lpOldBuffer = cpContext->lpBuffer;
        cpContext->lpBuffer = new char[cpContext->dwTotalSize + cpContext->dwSize];

        // Copy the old and read buffer into the new context buffer.
        memcpy(cpContext->lpBuffer, lpOldBuffer, cpContext->dwTotalSize);
        memcpy(cpContext->lpBuffer + cpContext->dwTotalSize, lpReadBuffer, cpContext->dwSize);

        // Free the memory allocated to the old and read buffers.
        delete [] lpOldBuffer;
        delete [] lpReadBuffer;
    }
    
    // Keep track of the total size.
    cpContext->dwTotalSize += cpContext->dwSize;
}

Trying It Out

Compile and run "AsyncDemo.cpp". If there are no errors, the application presents a dialog box. Click the Download Resources button to retrieve two documents simultaneously; both the headers and the contents of the document are displayed in different regions of the dialog box.

This example illustrates how WinHTTP functions can be used asynchronously.

Additional Error-Handling Considerations

The sample application currently does only enough error checking to demonstrate the principals involved, without slowing you down in potentially confusing code. For example, if the initial session handle is NULL, then the dialog box does not open. Ideally, the return value of every WinHTTP function would be checked in a similar fashion for failure, and an appropriate notification would be displayed for the user.

The handle-creating functions, WinHttpOpen, WinHttpConnect, and WinHttpOpenRequest, all return NULL if an error occurred. Then you can use the GetLastError function to determine what went wrong. In general, the flow of the application should be altered based on these errors.

For example, if WinHttpConnect fails, it should be retried before calling WinHttpOpenRequest.

The WinHTTP functions that are performed asynchronously, such as example WinHttpSendRequest, still activate the callback function. When an error occurs, a WINHTTP_CALLBACK_STATUS_REQUEST_ERROR notification is received. The following example shows how you might use the notification to provide feedback about any errors that take place. The WINHTTP_ASYNC_RESULT structure provides the error code and the function that caused the error.

        case WINHTTP_CALLBACK_STATUS_REQUEST_ERROR:
            pAR = (WINHTTP_ASYNC_RESULT *)lpvStatusInformation;
            swprintf_s( szBuffer,L"%s: REQUEST_ERROR - error %d, result %s", 
                      cpContext->szMemo, pAR->dwError, 
                      GetApiErrorString(pAR->dwResult));

            Cleanup(cpContext);
            break;

Currently, the application lets the user know that an error occurred, but it might be more useful to have the application give an option to retry the request upon getting an error such as, ERROR_WINHTTP_TIMEOUT.

 

 

Send comments about this topic to Microsoft

Build date: 11/1/2010