Программирование на Visual С++ - No.92 (WinInet)
Информационный Канал Subscribe.Ru |
|
РАССЫЛКА САЙТА
RSDN.RU |
Добрый день! Введение в WinInet Автор: Игорь ТкачёвЕщё вчера Вы даже и не думали о написании программ, использующих интернет протоколы, полагая, что это удел 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 );
InternetConnectЭта функция открывает FTP, HTTP или Gopher сессию для заданного сайта. HINTERNET InternetConnect( HINTERNET hInternet, LPCTSTR lpszServerName, INTERNET_PORT nServerPort, LPCTSTR lpszUsername, LPCTSTR lpszPassword, DWORD dwService, DWORD dwFlags, DWORD_PTR dwContext );
HttpOpenRequestHTTP запрос выполняется в несколько этапов: открытие запроса, определение HTTP заголовка, собственно отправка запроса, чтение и обработка данных. Эта функция, как следует из её названия, открывает HTTP запрос. HINTERNET HttpOpenRequest( HINTERNET hConnect, LPCTSTR lpszVerb, LPCTSTR lpszObjectName, LPCTSTR lpszVersion, LPCTSTR lpszReferer, LPCTSTR *lpszAcceptTypes, DWORD dwFlags, DWORD_PTR dwContext );
HttpSendRequestОтсылает запрос на сервер. BOOL HttpSendRequest( HINTERNET hRequest, LPCTSTR lpszHeaders, DWORD dwHeadersLength, LPVOID lpOptional, DWORD dwOptionalLength );
InternetReadFileЭта функция выполняет невероятно полезную работу, она позволяет читать данные результата запроса. BOOL InternetReadFile( HINTERNET hFile, LPVOID lpBuffer, DWORD dwNumberOfBytesToRead, LPDWORD lpdwNumberOfBytesRead );
InternetCloseHandleЭта функция закрывает любой из дескрипторов, созданных предыдущими функциями. BOOL InternetCloseHandle( HINTERNET hInternet );
Читаем страницуТеперь мы знаем всё необходимое, чтобы написать простую программу для чтения 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. Но лёгкие пути не для нас, тем более что нас интересует не простое чтение страниц, а полноценное общение с сервером. Что нам для этого потребуется?
Давайте с этого и начнём. Класс CHTTPReaderMFC содержит целый набор классов, позволяющих работать с WinInet, зачем нужен ещё один класс? Во-первых, классы MFC - это обёртки функций API, поэтому наш пример не будет выглядеть намного проще. Нам придётся создавать несколько объектов, по-прежнему помнить все необходимые флаги и частенько заглядывать в MSDN. С другой стороны, наш класс не будет универсальным, он будет работать только с HTTP протоколом и иметь минимально необходимый набор функций. Зато он будет простой и лёгкий в использовании. Во-вторых, MFC - это MFC, если мы не хотим использовать MFC, то мы будем вынуждены использовать API или... написать свой класс :o) Объявление классаВот интерфейс класса CHTTPReader: httpreader.zipclass 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 );
OpenInternet, OpenConnectionАвтоматически вызываются при запросе. Первая функция инициализирует WinInet и может использоваться для указания имени приложения. Вторая открывает HTTP сессию и позволяет указывать имя сервера. bool OpenInternet( LPCTSTR lpszAgent=TEXT("RSDN HTTP Reader") ); bool OpenConnection( LPCTSTR lpszServerName=NULL );
Get, PostОтправляют запрос на сервер на сервер методом или . bool Get( LPCTSTR lpszAction, LPCTSTR lpszReferer=NULL ); bool Post( LPCTSTR lpszAction, LPCTSTR lpszData, LPCTSTR lpszReferer=NULL );
GetDataЧитает данные с сервера. char *GetData( char *lpszBuffer, DWORD dwSize, DWORD *lpdwBytesRead=NULL ); char *GetData( DWORD *lpdwBytesRead=NULL );
Вторая версия функции читает данные во внутренний буфер, размер которого определяется с помощью вызова GetDataSize. При ошибке или завершении чтения данных возвращается NULL. GetDataSizeВозвращает размер данных, доступных для чтения. DWORD GetDataSize(); Использует для получения информации функцию HttpQueryInfo с параметром HTTP_QUERY_CONTENT_LENGTH. Я встречался с ситуацией, когда эта функция возвращала ноль, хотя после этого данные читались в полном объёме. Можно было бы использовать функцию InternetQueryDataAvailable, но с ней тоже не всё в порядке. Например, при чтении страницы ASP эта функция выдаёт не размер результирующей страницы, а размер самого скритпа, что, несомненно, является весьма интересной информацией, но совершенно бесполезной для нас. В результате, я не знаю и не могу предложить Вам абсолютно надёжного способа получить точную информацию о размере запрашиваемых данных. Скорее всего, это будет работать, но если Вы предполагаете использовать сервера, которые не можете заранее протестировать, то лучше не полагайтесь на эти функции. SetDataBufferУстанавливает размер внутреннего буфера. void SetDataBuffer( DWORD dwBufferSize );
CloseRequest, CloseConnection, CloseInternetВызываются автоматически при необходимости. Освобождают соответствующие ресурсы. SetDefaultHeaderПозволяет устанавливать HTTP заголовки. void SetDefaultHeader( LPCTSTR 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\" > "); data = skipstr(data,"<td align=\"right\" >"); getdata(buffer,data); cout << setw(7) << buffer << ' '; data = skipstr(data,"<td> "); 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. Браузер выведет две таблички, одна из которых пока пустая, вторая содержит список переменных сервера, анализ которых может быть весьма полезен. Для того чтобы проверить наш скрипт в действии давайте проделаем следующее:
Браузер опять отобразит var.asp, но на этот раз первая таблица будет заполнена именами полей и значениями формы ввода предыдущей страницы, т.е. Вы должны получить примерно следующее:
Теперь мы имеем полную картину, включая имена обычных и скрытых полей и их значения. Где эта улица, где этот дом? >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; } Запустите эту программу и Вы узнаете всё необходимое. Я не буду комментировать результаты запроса, прошу Вас лишь обратить внимание на название улицы и округа :) Комментариями отмечены места, которые нам необходимо рассмотреть для понимания происходящего.
ЗаключениеК сожалению, WinInet имеет ряд ограничений, затрудняющих его использование. Подробнее об этом можно узнать в следующих статьях базы знаний Microsoft:
Обойти эти ограничения можно, если создавать только одно соединение в одном процессе и запускать этот процесс под несистемным аккаунтом. В частности, совсем не сложно создать COM объект как локальный сервер, поместить в него всю работу с WinInet и для каждого создаваемого объекта запускать отдельный экземпляр приложения, предварительно установив соответствующие настройки Identity в DCOM Config. Этот способ будет работать, но вряд ли его назовёшь изящным. И, тем не менее, WinInet хорошо подходит для простых и средней сложности задач. Если Вам нужно добавить в программу, например, возможность online-регистрации, то, я надеюсь, теперь Вам понадобится для написания самой коммуникационной части не более получаса. Чтение WWW-страниц, как Вы могли убедиться, тоже не представляет никакой сложности. Фактически, программно Вы можете сделать всё, что Вы можете делать в браузере, включая процесс входа по паролю. Но, если Вы решите написать сам браузер... то, видимо, для этого лучше подойдут сокеты. Happy coding, |
http://subscribe.ru/
E-mail: ask@subscribe.ru |
Отписаться
Убрать рекламу |
В избранное | ||