ПРОГРАММИРОВАНИЕ    НА    V I S U A L   C + +
РАССЫЛКА САЙТА       
RSDN.RU  

    Выпуск No. 92 от 19 мая 2003 г.
   
Подписчиков: 21298 (+77) 

РАССЫЛКА ЯВЛЯЕТСЯ ЧАСТЬЮ ПРОЕКТА RSDN , НА САЙТЕ КОТОРОГО ВСЕГДА МОЖНО НАЙТИ ВСЮ НЕОБХОДИМУЮ РАЗРАБОТЧИКУ ИНФОРМАЦИЮ, СТАТЬИ, ФОРУМЫ, РЕСУРСЫ, ПОЛНЫЙ АРХИВ ПРЕДЫДУЩИХ ВЫПУСКОВ РАССЫЛКИ И МНОГОЕ ДРУГОЕ.

Добрый день!


 CТАТЬЯ

Введение в WinInet

wininet.zip - 20 KB

Ещё вчера Вы даже и не думали о написании программ, использующих интернет протоколы, полагая, что это удел web-программистов. Но, уже сегодня перед Вами стоит задача прочитать/записать, передать/принять, получить/послать что-либо из своей программы на какой-либо интернет-сервер. Какие средства для этого существуют? Сколько времени уйдёт на их изучение и эксперименты? Давайте рассмотрим один из способов, который позволяет решать большинство подобных задач в максимально короткие сроки.

Win32 Internet Extensions, или WinInet, представляет собой API для доступа к общим протоколам интернет, включая FTP, HTTP и Gopher. Это высокоуровневый API, позволяющий, в отличие от WinSock или TCP/IP, не заботиться о деталях реализации соответствующих интернет протоколов. Всего API содержит чуть менее сотни функций на все случаи жизни, но нам для начала работы с WinInet потребуется не более десятка.

Необходимый минимум

Рассмотрим простейший пример, позволяющий читать WWW страницу с заданного HTTP сервера. Общий алгоритм работы может быть следующим:


InternetOpen
  InternetConnect
    HttpOpenRequest
      HttpSendRequest
      InternetReadFile
    InternetCloseHandle
  InternetCloseHandle
InternetCloseHandle

Функции WinInet API

Разберём все функции по порядку и рассмотрим только те параметры, которые нам будут необходимы.

InternetOpen

Эта функция инициализирует WinInet и возвращает дескриптор, который необходим для вызова других функций WinInet. В случае неудачи возвращается NULL. Более подробную информацию об ошибке можно получить, вызвав функцию GetLastError, которая возвращает один из кодов, определённых в файле wininet.h.


HINTERNET WINAPI InternetOpen(
    LPCTSTR lpszAgent,
    DWORD dwAccessType,
    LPCTSTR lpszProxyName, 
    LPCTSTR lpszProxyBypass, 
    DWORD dwFlags
);

lpszAgent Задаёт имя приложения, которое используется как агент в HTTP протоколе. Сервер может определять агента с помощью переменной сервера HTTP_USER_AGENT. Если ваша программа собирается выдавать себя за MS Internet Explorer, передайте в этот параметр строчку
dwAccessType Задаёт необходимый тип доступа (прямой или через прокси). Мы будем использовать значение INTERNET_OPEN_TYPE_PRECONFIG, которое устанавливает тип доступа в соответствии с установками в реестре.
InternetConnect

Эта функция открывает FTP, HTTP или Gopher сессию для заданного сайта.


HINTERNET InternetConnect(
    HINTERNET hInternet,
    LPCTSTR lpszServerName,
    INTERNET_PORT nServerPort,
    LPCTSTR lpszUsername,
    LPCTSTR lpszPassword,
    DWORD dwService,
    DWORD dwFlags,
    DWORD_PTR dwContext
);

hInternet Дескриптор, полученный вызовом предыдущей функции.
lpszServerName Задаёт имя сервера. Это может быть обычное имя, например www.rsdn.ru, или адрес IP.
nServerPort Номер TCP/IP порта к которому мы собираемся подсоединиться. Мы будем использовать значене INTERNET_DEFAULT_HTTP_PORT для обычных соединений, либо INTERNET_DEFAULT_HTTPS_PORT для SSL соединений.
dwService Тип сервиса - FTP, HTTP или Gopher. Мы будем использовать INTERNET_SERVICE_HTTP.
HttpOpenRequest

HTTP запрос выполняется в несколько этапов: открытие запроса, определение HTTP заголовка, собственно отправка запроса, чтение и обработка данных. Эта функция, как следует из её названия, открывает HTTP запрос.


HINTERNET HttpOpenRequest(
    HINTERNET hConnect,
    LPCTSTR lpszVerb,
    LPCTSTR lpszObjectName,
    LPCTSTR lpszVersion,
    LPCTSTR lpszReferer,
    LPCTSTR *lpszAcceptTypes,
    DWORD dwFlags,
    DWORD_PTR dwContext
);

hConnect Дескриптор сессии.
lpszVerb Задаёт имя команды запроса. Мы будем использавать методы и .
lpszObjectName Имя целевого объекта. Это может быть просто HTML файл, скрипт или выполняемый модуль на сервере.
lpszReferer URL адрес предыдущей страницы. Чаще всего этот параметр игнорируется серверами, но если вдруг сервер перестанет подавать признаки жизни, попробуйте задать его, может помочь.
lpszAcceptTypes Определяет тип содержимого допускаемого клиентской стороной. Иногда MS IE передаёт сюда вот такую длинную строчку: , иногда это просто .
dwFlags Комбинация интернет флагов. Например, при использовании SSL соединений мы будем указывать флаг INTERNET_FLAG_SECURE. Так же нам будет полезен флаг INTERNET_FLAG_KEEP_CONNECTION, который позволяет удерживать соединение с сервером между запросами. Это бывает полезно, если мы хотим, чтобы сервер не забыл о нас во время сессий требующих входа по паролю.
HttpSendRequest

Отсылает запрос на сервер.


BOOL HttpSendRequest(
    HINTERNET hRequest,
    LPCTSTR lpszHeaders,
    DWORD dwHeadersLength,
    LPVOID lpOptional,
    DWORD dwOptionalLength
);

hRequest Дескриптор, полученный вызовом предыдущей функции.
lpszHeaders
dwHeadersLength
Позволяет добавлять дополнительные заголовки к запросу. Подробнее об HTTP заголовках можно узнать на www.w3.org.
lpOptional
dwOptionalLength
Указатель на данные, которые будут посланы на сервер вместе с запросом. Используется в методах и .
InternetReadFile

Эта функция выполняет невероятно полезную работу, она позволяет читать данные результата запроса.


BOOL InternetReadFile(
    HINTERNET hFile,
    LPVOID lpBuffer,
    DWORD dwNumberOfBytesToRead,
    LPDWORD lpdwNumberOfBytesRead
);

hFile Дескриптор сессии, полученный вызовом функции HttpOpenRequest.
lpBuffer
dwNumberOfBytesToRead
Адрес и размер буфера, в который будут записаны данные.
lpdwNumberOfBytesRead Число прочитанных байт.
InternetCloseHandle

Эта функция закрывает любой из дескрипторов, созданных предыдущими функциями.

BOOL InternetCloseHandle(
    HINTERNET hInternet
);

hInternet Дескриптор, полученный вызовом функций InternetOpen, InternetConnect или HttpOpenRequest.
Читаем страницу

Теперь мы знаем всё необходимое, чтобы написать простую программу для чтения HTML странички. Наш пример может выглядеть следующим образом:

newsreader1.zip


// newsreader1.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"

#include <windows.h>
#include <wininet.h>
#pragma comment(lib,"wininet")

#include <stdlib.h>
#include <fstream.h>

int main(int argc, char* argv[])
{
    bool ok = false;

    // инициализируем WinInet
    HINTERNET hInternet =
        ::InternetOpen(
            TEXT("WinInet Test"),
            INTERNET_OPEN_TYPE_PRECONFIG,
            NULL,NULL,
            0);

    if (hInternet != NULL) {
        // открываем HTTP сессию
        HINTERNET hConnect = 
            ::InternetConnect(
                hInternet,
                TEXT("www.rsdn.ru"),
                INTERNET_DEFAULT_HTTP_PORT,
                NULL,NULL,
                INTERNET_SERVICE_HTTP,
                0,
                1u);

        if (hConnect != NULL) {
            // открываем запрос
            HINTERNET hRequest = 
                ::HttpOpenRequest(
                    hConnect,
                    TEXT("GET"),
                    TEXT("news.asp"),
                    NULL,
                    NULL,
                    0,
                    INTERNET_FLAG_KEEP_CONNECTION,
                    1);

            if (hRequest != NULL) {
                // посылаем запрос
                BOOL bSend = ::HttpSendRequest(hRequest, NULL,0, NULL,0);
                if (bSend) {
                    // создаём выходной файл
                    ofstream fnews("news.html",ios::out|ios::binary);
                    if (fnews.is_open()) for (;;) {
                        // читаем данные
                        char  szData[1024];
                        DWORD dwBytesRead;
                        BOOL bRead = 
                            ::InternetReadFile(
                                hRequest,
                                szData,sizeof(szData)-1,
                                &dwBytesRead);

                        // выход из цикла при ошибке или завершении
                        if (bRead == FALSE  ||  dwBytesRead == 0)
                            break;

                        // сохраняем результат
                        szData[dwBytesRead] = 0;
                        fnews << szData;
                        
                        ok = true;
                    }
                }
                // закрываем запрос
                ::InternetCloseHandle(hRequest);
            }
            // закрываем сессию
            ::InternetCloseHandle(hConnect);
        }
        // закрываем WinInet
        ::InternetCloseHandle(hInternet);
    }

    // для полного счастья, запускаем считанную страничку
    if (ok)
        system("start news.html");

    return 0;
}

Как видите, всё довольно просто, хотя данный пример можно сделать ещё проще. Дело в том, что WinInet включает функцию InternetOpenUrl, которая может заменить пару HttpOpenRequest и HttpSendRequest. Но лёгкие пути не для нас, тем более что нас интересует не простое чтение страниц, а полноценное общение с сервером.

Что нам для этого потребуется?

  • Прежде всего, общение подразумевает диалог, следовательно, нам нужно уметь не только читать данные с сервера, но и уметь что-то ему передавать. Нет ничего проще. Для этого нужно только заменить параметр lpszVerb функции HttpOpenRequest с на или и использовать параметры lpOptional, dwOptionalLength функции HttpSendRequest для передачи собственно данных.
  • Так же для понимания происходяшего и отладки наших запросов нам будут нужны соответствующие средства.
  • И конечно же ни один добропорядочный C++ программист не будет спокойно созерцать приведённый выше код. У него обязательно появится желание завернуть всё это в класс.

Давайте с этого и начнём.

Класс CHTTPReader

MFC содержит целый набор классов, позволяющих работать с WinInet, зачем нужен ещё один класс? Во-первых, классы MFC - это обёртки функций API, поэтому наш пример не будет выглядеть намного проще. Нам придётся создавать несколько объектов, по-прежнему помнить все необходимые флаги и частенько заглядывать в MSDN. С другой стороны, наш класс не будет универсальным, он будет работать только с HTTP протоколом и иметь минимально необходимый набор функций. Зато он будет простой и лёгкий в использовании. Во-вторых, MFC - это MFC, если мы не хотим использовать MFC, то мы будем вынуждены использовать API или... написать свой класс :o)

Объявление класса

Вот интерфейс класса CHTTPReader:

httpreader.zip


class CHTTPReader
{
public:
    CHTTPReader (LPCTSTR lpszServerName=NULL,bool bUseSSL=false);
    ~CHTTPReader ();

    bool  OpenInternet     (LPCTSTR lpszAgent=TEXT("RSDN HTTP Reader"));
    void  CloseInternet    ();
    bool  OpenConnection   (LPCTSTR lpszServerName=NULL);
    void  CloseConnection  ();

    bool  Get              (LPCTSTR lpszAction,
		LPCTSTR lpszReferer=NULL);
    bool  Post             (LPCTSTR lpszAction,
		LPCTSTR lpszData,LPCTSTR lpszReferer=NULL);
    void  CloseRequest     ();

    char *GetData          (char *lpszBuffer,DWORD dwSize,
			DWORD *lpdwBytesRead=NULL);
    char *GetData          (DWORD *lpdwBytesRead=NULL);
    DWORD GetDataSize      ();
    void  SetDataBuffer    (DWORD dwBufferSize);

    void  SetDefaultHeader (LPCTSTR lpszDefaultHeader);
    DWORD GetError         () const;
};

Выделены те функции, которые мы будем использовать постоянно. Остальные могут быть полезны, но использовать их не обязательно. Ниже приведено описание методов класса CHTTPReader.

Методы
CHTTPReader

Конструктор.


CHTTPReader(
    LPCTSTR lpszServerName=NULL,
    bool bUseSSL=false
);

lpszServerName Имя сервера или адрес IP. Например www.rsdn.ru. Имя сервера также можно задать при вызове функции OpenConnection.
bUseSSL Использование Secure Sockets Layer.
OpenInternet, OpenConnection

Автоматически вызываются при запросе. Первая функция инициализирует WinInet и может использоваться для указания имени приложения. Вторая открывает HTTP сессию и позволяет указывать имя сервера.


bool OpenInternet(
    LPCTSTR lpszAgent=TEXT("RSDN HTTP Reader")
);
bool OpenConnection(
    LPCTSTR lpszServerName=NULL
);

lpszAgent Задаёт имя приложения, которое используется как агент в HTTP протоколе. Сервер может определять агента с помощью переменной сервера HTTP_USER_AGENT.
lpszServerName Задаёт имя сервера. Если этот параметр не задан и имя сервера не задано в конструкторе, то в качестве имени сервера используется .
Get, Post

Отправляют запрос на сервер на сервер методом или .


bool Get(
    LPCTSTR lpszAction,
    LPCTSTR lpszReferer=NULL
);
bool Post(
    LPCTSTR lpszAction,
    LPCTSTR lpszData,
    LPCTSTR lpszReferer=NULL
);

lpszAction Имя целевой страницы или файла.
lpszData Данные, передаваемые на сервер.
lpszReferer URL адрес предыдущей страницы. Иногда необходимо указывать для нормальной работы сервера.
GetData

Читает данные с сервера.


char *GetData(
    char *lpszBuffer,
    DWORD dwSize,
    DWORD *lpdwBytesRead=NULL
);

char *GetData(
    DWORD *lpdwBytesRead=NULL
);

lpszBuffer
dwSize
Адрес и размер буфера, в который будут записаны данные.
lpdwBytesRead Число прочитанных байт (если интересно).

Вторая версия функции читает данные во внутренний буфер, размер которого определяется с помощью вызова GetDataSize. При ошибке или завершении чтения данных возвращается NULL.

GetDataSize

Возвращает размер данных, доступных для чтения.


DWORD GetDataSize();

Использует для получения информации функцию HttpQueryInfo с параметром HTTP_QUERY_CONTENT_LENGTH. Я встречался с ситуацией, когда эта функция возвращала ноль, хотя после этого данные читались в полном объёме. Можно было бы использовать функцию InternetQueryDataAvailable, но с ней тоже не всё в порядке. Например, при чтении страницы ASP эта функция выдаёт не размер результирующей страницы, а размер самого скритпа, что, несомненно, является весьма интересной информацией, но совершенно бесполезной для нас. В результате, я не знаю и не могу предложить Вам абсолютно надёжного способа получить точную информацию о размере запрашиваемых данных. Скорее всего, это будет работать, но если Вы предполагаете использовать сервера, которые не можете заранее протестировать, то лучше не полагайтесь на эти функции.

SetDataBuffer

Устанавливает размер внутреннего буфера.


void SetDataBuffer(
    DWORD dwBufferSize
);

dwBufferSize Новый размер буфера.
CloseRequest, CloseConnection, CloseInternet

Вызываются автоматически при необходимости. Освобождают соответствующие ресурсы.

SetDefaultHeader

Позволяет устанавливать HTTP заголовки.


void SetDefaultHeader(
    LPCTSTR lpszDefaultHeader
);

lpszDefaultHeader Содержание заголовка.
GetError

Возвращает код GetLastError для последнего неудавшегося вызова функций WinInet.

Снова читаем страницу

На этот раз мы будем использовать класс CHTTPReader для чтения той же страницы новостей. Вот что из этого получилось:

newsreader2.zip


// newsreader2.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <stdlib.h>
#include <fstream.h>
#include "..\httpreader\HTTPReader.h"

int main(int argc, char* argv[])
{
    CHTTPReader rd();
    if (rd.Get()) {
        char *lpszData = rd.GetData();
        if (lpszData) {
            ofstream fnews("news.html",ios::out|ios::binary);
            if (fnews.is_open()) {
                fnews << lpszData;
                fnews.close();
                system("start news.html");
            }
        }
    }
    return 0;
}

Выделенные строчки - это собственно то, что относится к запросу, остальное - имитация бурной деятельности. Как видите теперь всё совсем просто.

Общение с сервером
Чтение курса валют

Давайте займёмся чем-нибудь более полезным, чем просто чтение страниц новостей. Например, как это ни странно, но у нас уже есть все средства для чтения данных о курсах валют на заданную дату с сервера ЦБ РФ. Следующий пример демонстрирует эту возможность.

newsreader3.zip


// newsreader3.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <stdlib.h>
#include <iostream.h>
#include <iomanip.h>
#include "..\httpreader\HTTPReader.h"

char *skipstr(const char *str1,const char *str2)
{
    char *str = strstr(str1,str2);
    if (str != NULL)
        str += strlen(str2);
    return str;
}

void getdata(char *buffer,const char *data) 
{
    int i=0;
    if (data)
        for (; *data != '<'; data++)
            buffer[i++] = *data;
    buffer[i] = 0;
}

int main(int argc, char* argv[])
{
    SYSTEMTIME stm;
    GetLocalTime(&stm);

    char action[100];
    sprintf(action,
        "/currency_base/D_print.asp?date_req=%02d.%02d.%02d",
                                             stm.wDay,
                                                  stm.wMonth,
                                                       stm.wYear);
    CHTTPReader rd();
    if (!rd.Get(action))
        return 1;

    char *data = rd.GetData();
    if (data == NULL)
        return 2;

    cout.setf(ios::left);

    while ((data=skipstr(data,"<tr bgcolor=\"#ffffff\">")) != NULL) {
        char buffer[50];

        data = skipstr(data,"<td align=\"right\" >");
        data = skipstr(data,"<td align=\"left\" >&nbsp;&nbsp;");
        data = skipstr(data,"<td align=\"right\" >");
        getdata(buffer,data);
        cout << setw(7) << buffer << ' ';

        data = skipstr(data,"<td>&nbsp;&nbsp;");
        getdata(buffer,data);
        CharToOem(buffer,buffer);
        cout << setw(26) << buffer << ' ';

        data = skipstr(data,"<td align=\"right\">");
        getdata(buffer,data);
        cout << buffer << endl;
    }
    return 0;
}

Фактически, мы формируем строку запроса, которая в браузере выглядит следующим образом , где вместо DD, MM, YYYY нужно подставить необходимую дату. Затем мы отправляем запрос на сервер и парсируем результат, выделяя необходимую информацию. Этот пример прекрасно работает, но имеет один существенный недостаток - он зависит от структуры HTML документа, которая может быть в любой момент изменена программистами ЦБ РФ.

Клиент - Сервер

Теперь настало время переключить наше внимание на разработку полноценного клиент-серверного приложения. То, что оно будет делать, не так важно, более важным является то, как оно это будет делать. Поэтому в качестве примера возьмём простой калькулятор, точнее даже умножитель. Вот текст ASP-скрипта нашей серверной части приложения:

calcasp.zip


<%
@Language=JScript 
@CODEPAGE=1251
%>

<%if (Request.Form.Count) {%>

<calc>
<x><%= Request.Form() %></x>
<y><%= Request.Form() %></y>
<z><%= Request.Form() * Request.Form() %></z>
</calc>

<%} else {%>

<html>
<body>
<form method="post" action="calc.asp">
<input name="x" value="2"></input><br>
*<br>
<input name="y" value="2"></input><br>
<input type="submit" name="submit" value=" = ">
</form>
</body>
</html>

<%}%>

Всё, что нам нужно для работы - это выделенный фрагмент, остальная часть текста приведена исключительно для демонстрации. Можете запустить этот скрипт на выполнение и убедиться, что он работает. Кликните по следующей ссылке: calc.asp.

Теперь займёмся клиентской частью нашего приложения. Передача данных на сервер производится методом или . У нас уже есть функция Post, которая умеет выполнять всю необходимую работу. Если Вы заметили, выделенный фрагмент текста в calc.asp выглядит необычно для ASP-скрипта. Всё правильно, наш скрипт возвращает данные в формате XML. А куда сейчас без него? :o) Наш клиент будет получать результат в XML-формате и использовать MSXML парсер для обработки результата:

calc.zip


// calc.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <iostream.h>
#include "..\httpreader\HTTPReader.h"
#import <msxml.dll>

void calc (CHTTPReader& rd,long x,long y)
{
    char buf[20];
    sprintf(buf,"x=%ld&y=%ld",x,y);

    if (rd.Post(,buf)) {
        char *data = rd.GetData();
        if (data) {
            MSXML::IXMLDOMDocumentPtr xml(__uuidof(MSXML::DOMDocument));
            xml->loadXML(data);
    
            MSXML::IXMLDOMElementPtr root = xml->documentElement;
            cout 
                << root->selectSingleNode(L"/calc/x")->text << " * "
                << root->selectSingleNode(L"/calc/y")->text << " = "
                << root->selectSingleNode(L"/calc/z")->text << endl;
        }
    }
}

int main(int argc, char* argv[])
{
    ::CoInitialize(NULL);

    try {
        CHTTPReader rd();
        for (long i=0; i<10; i++)
            calc(rd,i,9-i);
    } catch (_com_error& er) {
        cout << endl << er.ErrorMessage() << endl;
    }

    ::CoUninitialize();
    return 0;
}

Запустите этот пример и убедитесь в его работоспособности. Заметьте, что всю чёрную работу по умножению двух чисел выполняет RSDN.ru ;o) Конечно, этот способ не самый быстрый, но, тем не менее, если Вы будете испытывать проблемы с умножением, то всегда милости просим!

Вспомогательные средства

Для отладки наших запросов нам, прежде всего, потребуется интернет-сервер. В комплект Windows 2000 входит IIS 5.0, который нам вполне подойдёт, хотя, Вы можете использовать любой другой. Многие HTML формы помимо, видимых полей ввода, содержат скрытые поля, которые часто бывают размазаны по всему HTML документу. Выискивание этих полей задача не самая простая, особенно если документ создан программно и программисту незачем заботиться о его читабельности. Справится с этой проблемой нам поможет следующий скрипт:

var.zip


<%@
Language=VBScript 
@CODEPAGE=1251
%>
<html>
<body>

<p>
<table border="1">
<tr><td><b>Form Variable</b></td><td><b>Value</b></td></tr>
<% For Each strKey In Request.Form %> 
<tr><td><%= strKey %></td><td><%= Request.Form(strKey) %></td></tr>
<% Next %>
</table>
</p>

<p>
<table border="1">
<tr><td><b>Server Variable</b></td><td><b>Value</b></td></tr>
<% For Each strKey In Request.ServerVariables %> 
<tr><td><%= strKey %></td><td><%= Request.ServerVariables(strKey) %></td></tr>
<% Next %>
</table>
</p>

</body>
</head>

Скопируйте этот скрипт в каталог <X>:\Inetpub\wwwroot\, запустите браузер и введите адрес http://localhost/var.asp. Браузер выведет две таблички, одна из которых пока пустая, вторая содержит список переменных сервера, анализ которых может быть весьма полезен.

Для того чтобы проверить наш скрипт в действии давайте проделаем следующее:

  • Кликните по этой ссылке - http://www.usps.com/ncsc/lookups/lookup_zip+4.html. Это сайт почтового сервиса US, здесь можно уточнить почтовый адрес, если Вы не знаете, например, ZIP-код.
  • Кликните правой кнопкой мышки на странице и выберите пункт "View Source" контекстного меню.
  • В появившемся тексте страницы найдите тег <FORM... и его аттрибут ACTION. Сохраните значение атрибута, оно нам ещё понадобится.
  • Замените значение аттрибута на http://localhost/var.asp и сохраните файл на диске с расширением html. Допустим это будет c:\test.html.
  • Вернитесь в браузер и введите в поле адреса путь к нашему файлу: c:\test.html и жмите Enter.
  • Заполните форму какими-нибудь значениями и жмите кнопку Process.

Браузер опять отобразит var.asp, но на этот раз первая таблица будет заполнена именами полей и значениями формы ввода предыдущей страницы, т.е. Вы должны получить примерно следующее:

Form VariableValue
Firm123
Urbanization
Delivery Address123
City123
SubmitProcess
State12
Zip Code123

Теперь мы имеем полную картину, включая имена обычных и скрытых полей и их значения.

Где эта улица, где этот дом? >8(

Воспользуемся полученной информацией для ответа на этот любопытный вопрос.

getaddr.zip

// getaddr.cpp : Defines the entry point for the console application.
//

#include "stdafx.h"
#include <stdlib.h>
#include <fstream.h>
#include "..\httpreader\HTTPReader.h"

int main(int argc, char* argv[])
{
    CHTTPReader rd("www.usps.com");
    rd.SetDataBuffer(*);             // 1
    if (rd.Post(,  // 2
            "Firm=microsoft&"              // 3, 4
            "Urbanization=&"
            "Delivery%20Address=&"         // 5
            "City=redmond&"
            "State=wa&"
            "Zip%20Code=&"
            ))             // 6
    {
        char *lpszData = rd.GetData();
        if (lpszData) {
            ofstream fnews("addr.html",ios::out|ios::binary);
            if (fnews.is_open()) {
                fnews << lpszData;
                fnews.close();
                system("start addr.html");
            }
        }
    }
    return 0;
}

Запустите эту программу и Вы узнаете всё необходимое. Я не буду комментировать результаты запроса, прошу Вас лишь обратить внимание на название улицы и округа :)

Комментариями отмечены места, которые нам необходимо рассмотреть для понимания происходящего.

  1. Это тот самый случай, о котором я говорил раньше, для данного сервера функция HttpQueryInfo возвращает длинну содержимого запроса равную нулю. В этой строчке устанавливается размер буфера, достаточный для приёма данных.
  2. Этот адрес я взял из атрибута ACTION тега <FORM... первоначальной страницы. Заметьте, что в данном примере нам совсем не обязательно её читать, мы сразу вызываем скрипт, выполняющий запрос.
  3. Символ '=' служит разделителем между именем поля и его значением.
  4. Символ '&' служит разделителем между полями.
  5. Пробелы и другие специальные символы необходимо заменять их шестнадцатиричными эквивалентами.
  6. Submit - это скрытое поле. В данном случае оно только одно.

ПРИМЕЧАНИЕ
Если Вы заметили, порядок следования полей в нашем запросе и в списке полей, выданных скриптом var.asp отличаются. Если следовать последнему, то сервер вернёт страницу с информацией о том, что имя штата, которое мы ввели не найдено в базе данных. Скрипт ASP выдаёт список полей формы именно в таком неверном порядке. С другой стороны, судя по сообщению, на сервере вообще не используются имена полей, а только их нумерация.

Заключение

К сожалению, WinInet имеет ряд ограничений, затрудняющих его использование. Подробнее об этом можно узнать в следующих статьях базы знаний Microsoft:

  1. INFO: WinInet Limits Connections Per Server (Q183110).
    WinInet ограничивает число соединений для одного сервера, которое может быть равно двум или четырём. Такое ограничение связано с соглашениями HTTP спецификации и стандарта.
  2. INFO: WinInet Not Supported for Use in Services (Q238425)
    WinInet не может использоваться сервисами Windows NT, в частности IIS, который тоже является сервисом. Это является следствием того, что сервисы работают под системным аккаунтом и не могут читать данные из раздела HKEY_CURRENT_USER реестра. В свою очередь, для нормальной работы WinInet необходима информация о настройках SSL, прокси и т.п.

Обойти эти ограничения можно, если создавать только одно соединение в одном процессе и запускать этот процесс под несистемным аккаунтом. В частности, совсем не сложно создать COM объект как локальный сервер, поместить в него всю работу с WinInet и для каждого создаваемого объекта запускать отдельный экземпляр приложения, предварительно установив соответствующие настройки Identity в DCOM Config. Этот способ будет работать, но вряд ли его назовёшь изящным.

И, тем не менее, WinInet хорошо подходит для простых и средней сложности задач. Если Вам нужно добавить в программу, например, возможность online-регистрации, то, я надеюсь, теперь Вам понадобится для написания самой коммуникационной части не более получаса. Чтение WWW-страниц, как Вы могли убедиться, тоже не представляет никакой сложности. Фактически, программно Вы можете сделать всё, что Вы можете делать в браузере, включая процесс входа по паролю. Но, если Вы решите написать сам браузер... то, видимо, для этого лучше подойдут сокеты.

Happy coding,
Игорь.



Ведущий рассылки: Алекс Jenter   jenter@rsdn.ru
Публикуемые в рассылке материалы принадлежат сайту RSDN.

| Предыдущие выпуски     | Статистика рассылки