WinInet: скачка\докачка файлов по http-протоколу

Wininet представляет собой api для доступа к протоколам интернет, таким как http, ftp, gopher. С помощью данного набора api можно с легкостью скачивать файлы, страницы, писать разные интересные программы, не сильно задумываясь о таких вещах, как сокеты, таймауты, протоколы, etc. В данной статье будет рассмотрено, как закачать файл по протоколу http, используя функции wininet'а.

Для начала нам нужно открыть сессию, т.е. вызвать функцию InternetOpen:
INTERNETAPI
HINTERNET
WINAPI
InternetOpen(
    IN LPCTSTR lpszAgent,
    IN DWORD dwAccessType,
    IN LPCTSTR lpszProxy OPTIONAL,
    IN LPCTSTR lpszProxyBypass OPTIONAL,
    IN DWORD dwFlags
    );

все, что мы здесь устанавливаем (остальные значения можно смело забивать нулем), это 1ый параметр lpszAgent - то, как нас будет определять сервер (IE, Opera, etc.) и 2ый параметр - тип доступа (прямой, прокси или "предустановленный"). Чаще всего будет использоваться "предустановленный" тип соединения, т.е. настройки соединения будут браться из системного реестра. Этот тип соответствует константе INTERNET_OPEN_TYPE_PRECONFIG. Настройки же можно изменить в броузере Internet Explorer зайдя в Tools->Internet Options->Connections. В случае, если вам надо, например, использовать другой прокси, то используйте dwAccessType=INTERNET_OPEN_TYPE_PROXY. Пример:
HINTERNET session = InternetOpen("Our Agent", 
  INTERNET_OPEN_TYPE_PROXY, 
  "192.168.0.1:3128", /* наша прокся 192.168.0.1 порт 3128 */
  "", 0);

Также можно посмотреть в сторону функции InternetSetOption:
BOOLAPI
InternetSetOption(
    IN HINTERNET hInternet OPTIONAL,
    IN DWORD dwOption,
    IN LPVOID lpBuffer,
    IN DWORD dwBufferLength
    );

INTERNET_PROXY_INFO pi;
pi.dwAccessType = INTERNET_OPEN_TYPE_PROXY;
pi.lpszProxy = "192.168.0.1:3128";
pi.lpszProxyBypass = "";
InternetSetOption(session, INTERNET_OPTION_PROXY, (LPVOID)&pi, sizeof(pi));
// пример аналогичный предыдущему :)

Сессия открыли, теперь можно получить хэндл необходимого файла в сети. Для этого нам нужна функция InternetOpenUrl. Эта функция работает независимо от протокола (wininet поддерживает протоколы http, ftp, gopher).
INTERNETAPI
HINTERNET
WINAPI
InternetOpenUrl(
    IN HINTERNET hInternet,
    IN LPCTSTR lpszUrl,
    IN LPCTSTR lpszHeaders OPTIONAL,
    IN DWORD dwHeadersLength,
    IN DWORD dwFlags,
    IN DWORD dwContext
    );

В качестве параметров функция принимает хэндл открытой сессии (см. InternetOpen), строчку с указанным файлом\страницей. Остальные параметры нам не важны (опять нули). Кстати, из всех функций, нами используемых, эта функция самая тормозная. Думаю, не надо обьяснять почему.

Все файл "открыли", ошибок при этом не возникло, можно из этого файла что-нибудь прочитать. Для этого нам нужна функция InternetReadFile, к-рая очень похоже на ReadFile для локальных файлов. Вот ее описание:
BOOLAPI
InternetReadFile(
    IN HINTERNET hFile,
    IN LPVOID lpBuffer,
    IN DWORD dwNumberOfBytesToRead,
    OUT LPDWORD lpdwNumberOfBytesRead
    );

На вход подается хэндл файла (hFile), буфер для чтения (lpBuffer), сколько байт прочитать (dwNumberOfBytesToRead). В моем примере качаются блоки по 64 кбайт. Если все прошло удачно, то функция возвратит TRUE и число прочитанных байт в lpdwNumberOfBytesRead. В противном случае, это может означать, что произошел разрыв связи, мы прочитали файл до конца либо что-то еще. Если произошел обрыв связи, то нам нужно сделать реконнект (закрыть сессию и открыть ее вновь через некоторый промежуток времени), вернуть file pointer на то место, до к-рого мы прочитали файл, и продолжить читать файл. Для "перемещения" по файлу нам нужен аналог функции SetFilePointer для wininet'a - InternetSetFilePointer.
INTERNETAPI
DWORD
WINAPI
InternetSetFilePointer(
    IN HINTERNET hFile,
    IN LONG  lDistanceToMove,
    IN PVOID pReserved,
    IN DWORD dwMoveMethod,
    IN DWORD dwContext
    );

lDistanceToMove указывает на сколько сдвинуть fp относительно начала (dwMoveMethod=FILE_BEGIN), текущей позиции (FILE_CURRENT) или конца файла (FILE_END).

После того, как файл успешно закачен, хорошим тоном является закрытие всех открытых ресурсов. Для этого используется аналог функции CloseHandle - InternetCloseHandle.

Также для определения разных состояний потребуется функция HttpQueryInfo. Синтаксис такой:
BOOLAPI
HttpQueryInfo(
    IN HINTERNET hRequest,
    IN DWORD dwInfoLevel,
    IN OUT LPVOID lpBuffer OPTIONAL,
    IN OUT LPDWORD lpdwBufferLength,
    IN OUT LPDWORD lpdwIndex OPTIONAL
    );

Это универсальная функция, к-рая позволяет определить много различных параметров (подробности см. в wininet.h). В примере она используется для определения размера файла и определения нужна ли авторизация на сервере для скачки файла. В качестве параметров требуется hRequest, хэндл открытый через InternetOpen или HttpOpenRequest, dwInfoLevel - флаги, к-рые определяют действие функции (определение размера файла - HTTP_QUERY_CONTENT_LENGTH|HTTP_QUERY_FLAG_NUMBER, проверка на ощибки - HTTP_QUERY_FLAG_NUMBER | HTTP_QUERY_STATUS_CODE, см также wininet.h), lpBuffer - буфер, в к-рый будет возвращен результат, lpdwBufferLength - его размер (буфера), lpdwIndex - как всегда 0.

Возможнен случай, когда нужна авторизация на сервере. Тоже не проблема: проверяется это с помощью функции HttpQueryInfo, а логин\пасс устанавливается опять с помощью InternetSetOption.
// fd - хэндл файла
// login - имя изера 
// pass - пароль
char status[32];
unsigned long size = sizeof(status)/sizeof(char);
if (HttpQueryInfo(fd, HTTP_QUERY_FLAG_NUMBER|HTTP_QUERY_STATUS_CODE, status, &size, NULL)) {
  if ((*(int *)status)==HTTP_STATUS_PROXY_AUTH_REQ) {
    InternetSetOption(fd, INTERNET_OPTION_PROXY_USERANAME, login, strlen(login));
    InternetSetOption(fd, INTERNET_OPTION_PROXY_PASSWORD, pass, strlen(pass));
  }
}


С функциями определились, теперь можно показать пример закачки файла:
// большинство проверок пропущено
HINTERNET session, file;
session = InternetOpen("Our Agent", PRE_CONFIG_INTERNET_ACCESS, 0, 0, 0);
file = InternetOpenUrl(session, "http://www.oursite.com/ourfile.zip", 0, 0, 0, 0);
char buf[64000];
unsigned long len, offs;
offs = 0;
do {
   InternetReadFile(file, buf, sizeof(buf), &len);
   if (len>=0) {
      // данные прочитались
      // можно с ними что-нибудь сделать, например, записать в файл
      offs += len;
   } else {
      // анализируем ошибку
      int error = GetLastError();
      // пытаемся восстановить закачку
      // закрываем сессию
      InternetCloseHandle(file);
      InternetCloseHandle(session);
      // ждем какое-то время, например, 1 секунду
      Sleep(1000);
      // открываем сессию по-новой
      session = InternetOpen("Our Agent", PRE_CONFIG_INTERNET_ACCESS, 0, 0, 0);
      file = InternetOpenUrl(session, "http://www.oursite.com/ourfile.zip", 0, 0, 0, 0);
      // перемещаем указатель
      InternetSetFilePointer(file, offs, 0, FILE_BEGIN, 0);
      // продолжаем качать файл
   }
} while (len);
InternetCloseHandle(file);
InternetCloseHandle(session);


Прикрепренный проект представляет собой простенькую однопоточную качалку, к-рая тем не менее поддерживает докачку файла. В проекте показана работа с wininet'ом и ini-файлами (для некоторых личностей и для этого нужен фак ;-). Программка закачивает один файл частями по BUF_SIZE и записывает их в файл. Также в конфиг-файл программки (download.exe.ini) пишется сколько закачано, и в случае чего по этим данным происходит "откат" закачки. Также есть проверка ошибок (простая :), определение длины файла, поддерживаются сайты с авторизацией. Проект нормально компилиться под gcc(mingw), bc5.5 и vc6.0\7.1. c-like style.

Некоторые замечания:
1. Wininet имеет ограничение на число соединений для одного сервера (2 или 4). Лечиться здесь (максимум, как я понимаю, все же есть, не больше 10):
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Internet Settings\MaxConnectionsPer1_0Server
HKEY_LOCAL_MACHINE\Software\Microsoft\Windows\CurrentVersion\Internet Settings\MaxConnectionsPerServer
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\MaxConnectionsPer1_0Server
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Internet Settings\MaxConnectionsPerServer


2. Нередко бывает, что нужно пропарсить строчку с урлой на предмет того, какой протокол используется, на каком порту, какой сервер, какой файл, etc. Можно это делать самому, но в wininet'e уже есть схожие функции. Вот небольшой кусок кода из mfc (немного переработаный мной под свои нужды), к-рый это делает:
static int ParseUrlEx(char *pstrURL, LPURL_COMPONENTS lpComponents, 
               DWORD& dwServiceType, INTERNET_PORT& nPort, DWORD dwFlags)
{
    assert(lpComponents!=NULL && pstrURL!=NULL);
    if (lpComponents==NULL || pstrURL==NULL)
        return 0;

    assert(lpComponents->dwHostNameLength==0 || lpComponents->lpszHostName);
    assert(lpComponents->dwUrlPathLength==0 || lpComponents->lpszUrlPath);
    assert(lpComponents->dwUserNameLength==0 || lpComponents->lpszUserName);
    assert(lpComponents->dwPasswordLength==0 || lpComponents->lpszPassword);

    LPTSTR pstrCanonicalizedURL;
    TCHAR szCanonicalizedURL[INTERNET_MAX_URL_LENGTH];
    DWORD dwNeededLength = INTERNET_MAX_URL_LENGTH;
    int bRetVal;
    BOOL bMustFree = FALSE;
    DWORD dwCanonicalizeFlags = dwFlags & (ICU_NO_ENCODE | ICU_DECODE | 
        ICU_NO_META | ICU_ENCODE_SPACES_ONLY | ICU_BROWSER_MODE);
    DWORD dwCrackFlags = dwFlags & (ICU_ESCAPE | ICU_USERNAME);

    bRetVal = InternetCanonicalizeUrl(pstrURL, szCanonicalizedURL,
        &dwNeededLength, dwCanonicalizeFlags);

    if (!bRetVal) {
        if (GetLastError()!=ERROR_INSUFFICIENT_BUFFER)
            return 0;

        pstrCanonicalizedURL = (LPTSTR)malloc(dwNeededLength*sizeof(TCHAR));
        bMustFree = TRUE;
        bRetVal = InternetCanonicalizeUrl(pstrURL, pstrCanonicalizedURL,
            &dwNeededLength, dwCanonicalizeFlags);
        if (!bRetVal) {
            free(pstrCanonicalizedURL);
            return 0;
        }
    } else {
        pstrCanonicalizedURL = szCanonicalizedURL;
    }

    bRetVal = InternetCrackUrl(pstrCanonicalizedURL, 0, 
        dwCrackFlags, lpComponents);
    if (bMustFree) free(pstrCanonicalizedURL);

#define PARSEURLX(x) case x: dwServiceType = x; break;

    if (!bRetVal)
        dwServiceType = INTERNET_SCHEME_UNKNOWN;
    else {
        nPort = lpComponents->nPort;
        switch (lpComponents->nScheme)
        {
            PARSEURLX(INTERNET_SCHEME_FTP);
            PARSEURLX(INTERNET_SCHEME_GOPHER);
            PARSEURLX(INTERNET_SCHEME_HTTP);
            PARSEURLX(INTERNET_SCHEME_HTTPS);
            PARSEURLX(INTERNET_SCHEME_FILE);
            PARSEURLX(INTERNET_SCHEME_NEWS);
            PARSEURLX(INTERNET_SCHEME_MAILTO);
            default:
                dwServiceType = INTERNET_SCHEME_UNKNOWN;
        }
    }
    return bRetVal;
}

int ParseUrl(char *pstrURL, DWORD& dwServiceType, 
             char *strServer, char *strObject, INTERNET_PORT& nPort)
{
    dwServiceType = INTERNET_SCHEME_UNKNOWN;
    if (!pstrURL) return 0;

    URL_COMPONENTS urlComp;
    memset((void *)&urlComp, 0, sizeof(URL_COMPONENTS));
    urlComp.dwStructSize = sizeof(URL_COMPONENTS);
    urlComp.dwHostNameLength = INTERNET_MAX_URL_LENGTH;
    urlComp.lpszHostName = strServer;
    urlComp.dwUrlPathLength = INTERNET_MAX_URL_LENGTH;
    urlComp.lpszUrlPath = strObject;
    
    return ParseUrlEx(pstrURL, &urlComp, dwServiceType, 
        nPort, ICU_BROWSER_MODE);
}