Methods to limit bandwidth usage with WinHTTP APIs

1.8k views Asked by At

I'm using WinHTTP APIs in a C++ code similar to the one at the bottom of this article. It runs from my Windows service, and is used to download updates in the background. The code works fine, except that I've received complaints that when it is downloading an update that code uses up too much bandwidth available on a client computer.

Is there a way to make those WinHTTP APIs, WinHttpQueryDataAvailable and WinHttpReadData in particular, limit how much bandwidth they use? Say, up to 30% of the available bandwidth.

PS. For the ease of reference I'm going to copy the code I'm referring from the MSDN article:

DWORD dwSize = 0;
DWORD dwDownloaded = 0;
LPSTR pszOutBuffer;
BOOL  bResults = FALSE;
HINTERNET  hSession = NULL, 
           hConnect = NULL,
           hRequest = NULL;

// Use WinHttpOpen to obtain a session handle.
hSession = WinHttpOpen( L"WinHTTP Example/1.0",  
                        WINHTTP_ACCESS_TYPE_DEFAULT_PROXY,
                        WINHTTP_NO_PROXY_NAME, 
                        WINHTTP_NO_PROXY_BYPASS, 0);

// Specify an HTTP server.
if (hSession)
    hConnect = WinHttpConnect( hSession, L"www.microsoft.com",
                               INTERNET_DEFAULT_HTTPS_PORT, 0);

// Create an HTTP request handle.
if (hConnect)
    hRequest = WinHttpOpenRequest( hConnect, L"GET", NULL,
                                   NULL, WINHTTP_NO_REFERER, 
                                   WINHTTP_DEFAULT_ACCEPT_TYPES, 
                                   WINHTTP_FLAG_SECURE);

// Send a request.
if (hRequest)
    bResults = WinHttpSendRequest( hRequest,
                                   WINHTTP_NO_ADDITIONAL_HEADERS,
                                   0, WINHTTP_NO_REQUEST_DATA, 0, 
                                   0, 0);


// End the request.
if (bResults)
    bResults = WinHttpReceiveResponse( hRequest, NULL);

// Keep checking for data until there is nothing left.
if (bResults)
{
    do 
    {
        // Check for available data.
        dwSize = 0;
        if (!WinHttpQueryDataAvailable( hRequest, &dwSize)) 
        {
            printf( "Error %u in WinHttpQueryDataAvailable.\n",
                    GetLastError());
            break;
        }

        // No more available data.
        if (!dwSize)
            break;

        // Allocate space for the buffer.
        pszOutBuffer = new char[dwSize+1];
        if (!pszOutBuffer)
        {
            printf("Out of memory\n");
            break;
        }

        // Read the Data.
        ZeroMemory(pszOutBuffer, dwSize+1);

        if (!WinHttpReadData( hRequest, (LPVOID)pszOutBuffer, 
                              dwSize, &dwDownloaded))
        {                                  
            printf( "Error %u in WinHttpReadData.\n", GetLastError());
        }
        else
        {
            printf("%s", pszOutBuffer);
        }

        // Free the memory allocated to the buffer.
        delete [] pszOutBuffer;

        // This condition should never be reached since WinHttpQueryDataAvailable
        // reported that there are bits to read.
        if (!dwDownloaded)
            break;

    } while (dwSize > 0);
}
else
{
    // Report any errors.
    printf( "Error %d has occurred.\n", GetLastError() );
}

// Close any open handles.
if (hRequest) WinHttpCloseHandle(hRequest);
if (hConnect) WinHttpCloseHandle(hConnect);
if (hSession) WinHttpCloseHandle(hSession);

EDIT: While following up on @RemyLebeau suggestions, I created a test C++ project (you can download it from here) that attempts to calculate the current download rate used by the method above and use "Sleep" API to throttle itself. Unfortunately the results I'm getting from it are quite unexpected. I made a screenshot:

enter image description here

See the difference between my reading and what Task Manager is giving me. (Note that nothing was using bandwidth at the time I was running these tests.)

I must be missing something. The question is what?

1

There are 1 answers

9
Remy Lebeau On

It is not always easy to throttle by "30% of available bandwidth", as you would have to know what the "available bandwidth" actually is, and that may not always be easy to determine programmably. I suppose you could clock each loop iteration to calculate the probable bandwidth based on how long each read takes. But that very well may fluctuate as bandwidth is used for other things, and as you throttle your bandwidth usage, your calculation of the available bandwidth would be affected.

What is more common (and typically easier) to implement is to throttle by desired "bytes per (milli)second" instead. You can't throttle WinHttpReadData() itself, but you can throttle how often you call it. Just keep track of how many bytes you are reading and sleep your loop iterations so you don't read too many bytes past your desired throttle speed - sleeping longer to slow down, and sleeping shorter to speed up, eg:

// Keep checking for data until there is nothing left.
if (bResults)
{
    char *pszOutBuffer = NULL;
    DWORD dwOutBufferSize = 0;

    do 
    {
        // Check for available data.

        // RL: personally, I would not bother with WinHttpQueryDataAvailable()
        // at all.  Just allocate a fixed-size buffer and let WinHttpReadData()
        // tell you when there is no more data to read...

        dwSize = 0;
        if (!WinHttpQueryDataAvailable( hRequest, &dwSize)) 
        {
            printf( "Error %u in WinHttpQueryDataAvailable.\n", GetLastError());
            break;
        }

        // No more available data.
        if (!dwSize)
            break;

        // (re)Allocate space for the buffer.
        if (dwSize > dwOutBufferSize)
        {
            delete [] pszOutBuffer;
            pszOutBuffer = NULL;
            dwOutBufferSize = 0;

            pszOutBuffer = new char[dwSize];
            if (!pszOutBuffer)
            {
                printf("Out of memory\n");
                break;
            }

            dwOutBufferSize = dwSize;
        }

        // Read the Data.
        DWORD dwStart = GetTickCount();
        if (!WinHttpReadData(hRequest, pszOutBuffer, dwSize, &dwDownloaded))
        {                                  
            printf("Error %u in WinHttpReadData.\n", GetLastError());
            break;
        }
        DWORD dwEnd = GetTickCount();
        DWORD dwDownloadTime = (dwEnd >= dwStart) ? (dwEnd - dwStart) : ((MAXDWORD-dwStart)+dwEnd);
        if (dwDownloadTime == 0) dwDownloadTime = 1;

        printf("%.*s", dwDownloaded, pszOutBuffer);

        // throttle by bits/sec
        //
        // todo: use a waitable object to sleep on, or use smaller
        // sleeps more often, if you need to abort a transfer in
        // progress during long sleeps...

        __int64 BitsPerSec = (__int64(dwDownloaded) * 8) * 1000) / dwDownloadTime;
        if (BitsPerSec > DesiredBitsPerSec)
            Sleep( ((BitsPerSec - DesiredBitsPerSec) * 1000) / DesiredBitsPerSec );
    }
    while (true);

    // Free the memory allocated to the buffer.
    delete [] pszOutBuffer;
}
else
{
    // Report any errors.
    printf( "Error %d has occurred.\n", GetLastError() );
}