Сообщений 27 Оценка 490 [+2/-0] Оценить |
Ещё вчера Вы даже и не думали о написании программ, использующих интернет протоколы, полагая, что это удел 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 и возвращает дескриптор, который необходим для вызова других функций 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, передайте в этот параметр строчку "Mozilla/4.0 (compatible; MSIE 6.0b; Windows NT 5.0; .NET CLR 1.0.2914) " |
dwAccessType | Задаёт необходимый тип доступа (прямой или через прокси). Мы будем использовать значение INTERNET_OPEN_TYPE_PRECONFIG, которое устанавливает тип доступа в соответствии с установками в реестре. |
Эта функция открывает 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. |
HTTP запрос выполняется в несколько этапов: открытие запроса, определение HTTP заголовка, собственно отправка запроса, чтение и обработка данных. Эта функция, как следует из её названия, открывает HTTP запрос.
HINTERNET HttpOpenRequest( HINTERNET hConnect, LPCTSTR lpszVerb, LPCTSTR lpszObjectName, LPCTSTR lpszVersion, LPCTSTR lpszReferer, LPCTSTR *lpszAcceptTypes, DWORD dwFlags, DWORD_PTR dwContext ); |
hConnect | Дескриптор сессии. |
lpszVerb | Задаёт имя команды запроса. Мы будем использавать методы "GET" и "POST". |
lpszObjectName | Имя целевого объекта. Это может быть просто HTML файл, скрипт или выполняемый модуль на сервере. |
lpszReferer | URL адрес предыдущей страницы. Чаще всего этот параметр игнорируется серверами, но если вдруг сервер перестанет подавать признаки жизни, попробуйте задать его, может помочь. |
lpszAcceptTypes | Определяет тип содержимого допускаемого клиентской стороной. Иногда MS IE передаёт сюда вот такую длинную строчку: "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/msword, application/vnd.ms-excel, application/vnd.ms-powerpoint, */*", иногда это просто "*/*". |
dwFlags | Комбинация интернет флагов. Например, при использовании SSL соединений мы будем указывать флаг INTERNET_FLAG_SECURE. Так же нам будет полезен флаг INTERNET_FLAG_KEEP_CONNECTION, который позволяет удерживать соединение с сервером между запросами. Это бывает полезно, если мы хотим, чтобы сервер не забыл о нас во время сессий требующих входа по паролю. |
Отсылает запрос на сервер.
BOOL HttpSendRequest( HINTERNET hRequest, LPCTSTR lpszHeaders, DWORD dwHeadersLength, LPVOID lpOptional, DWORD dwOptionalLength ); |
hRequest | Дескриптор, полученный вызовом предыдущей функции. |
lpszHeaders dwHeadersLength |
Позволяет добавлять дополнительные заголовки к запросу. Подробнее об HTTP заголовках можно узнать на www.w3.org. |
lpOptional dwOptionalLength |
Указатель на данные, которые будут посланы на сервер вместе с запросом. Используется в методах "POST" и "PUT". |
Эта функция выполняет невероятно полезную работу, она позволяет читать данные результата запроса.
BOOL InternetReadFile( HINTERNET hFile, LPVOID lpBuffer, DWORD dwNumberOfBytesToRead, LPDWORD lpdwNumberOfBytesRead ); |
hFile | Дескриптор сессии, полученный вызовом функции HttpOpenRequest. |
lpBuffer dwNumberOfBytesToRead |
Адрес и размер буфера, в который будут записаны данные. |
lpdwNumberOfBytesRead | Число прочитанных байт. |
Эта функция закрывает любой из дескрипторов, созданных предыдущими функциями.
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. Но лёгкие пути не для нас, тем более что нас интересует не простое чтение страниц, а полноценное общение с сервером.
Что нам для этого потребуется?
Давайте с этого и начнём.
MFC содержит целый набор классов, позволяющих работать с 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( LPCTSTR lpszServerName=NULL, bool bUseSSL=false ); |
lpszServerName | Имя сервера или адрес IP. Например www.rsdn.ru. Имя сервера также можно задать при вызове функции OpenConnection. |
bUseSSL | Использование Secure Sockets Layer. |
Автоматически вызываются при запросе. Первая функция инициализирует WinInet и может использоваться для указания имени приложения. Вторая открывает HTTP сессию и позволяет указывать имя сервера.
bool OpenInternet( LPCTSTR lpszAgent=TEXT("RSDN HTTP Reader") ); bool OpenConnection( LPCTSTR lpszServerName=NULL ); |
lpszAgent | Задаёт имя приложения, которое используется как агент в HTTP протоколе. Сервер может определять агента с помощью переменной сервера HTTP_USER_AGENT. |
lpszServerName | Задаёт имя сервера. Если этот параметр не задан и имя сервера не задано в конструкторе, то в качестве имени сервера используется "localhost". |
Отправляют запрос на сервер на сервер методом "GET" или "POST".
bool Get( LPCTSTR lpszAction, LPCTSTR lpszReferer=NULL ); bool Post( LPCTSTR lpszAction, LPCTSTR lpszData, LPCTSTR lpszReferer=NULL ); |
lpszAction | Имя целевой страницы или файла. |
lpszData | Данные, передаваемые на сервер. |
lpszReferer | URL адрес предыдущей страницы. Иногда необходимо указывать для нормальной работы сервера. |
Читает данные с сервера.
char *GetData( char *lpszBuffer, DWORD dwSize, DWORD *lpdwBytesRead=NULL ); char *GetData( DWORD *lpdwBytesRead=NULL ); |
lpszBuffer dwSize |
Адрес и размер буфера, в который будут записаны данные. |
lpdwBytesRead | Число прочитанных байт (если интересно). |
Вторая версия функции читает данные во внутренний буфер, размер которого определяется с помощью вызова GetDataSize. При ошибке или завершении чтения данных возвращается NULL.
Возвращает размер данных, доступных для чтения.
DWORD GetDataSize(); |
Использует для получения информации функцию HttpQueryInfo с параметром HTTP_QUERY_CONTENT_LENGTH. Я встречался с ситуацией, когда эта функция возвращала ноль, хотя после этого данные читались в полном объёме. Можно было бы использовать функцию InternetQueryDataAvailable, но с ней тоже не всё в порядке. Например, при чтении страницы ASP эта функция выдаёт не размер результирующей страницы, а размер самого скритпа, что, несомненно, является весьма интересной информацией, но совершенно бесполезной для нас. В результате, я не знаю и не могу предложить Вам абсолютно надёжного способа получить точную информацию о размере запрашиваемых данных. Скорее всего, это будет работать, но если Вы предполагаете использовать сервера, которые не можете заранее протестировать, то лучше не полагайтесь на эти функции.
Устанавливает размер внутреннего буфера.
void SetDataBuffer(
DWORD dwBufferSize
);
|
dwBufferSize | Новый размер буфера. |
Вызываются автоматически при необходимости. Освобождают соответствующие ресурсы.
Позволяет устанавливать HTTP заголовки.
void SetDefaultHeader(
LPCTSTR lpszDefaultHeader
);
|
lpszDefaultHeader | Содержание заголовка. |
Возвращает код 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("www.rsdn.ru"); if (rd.Get("news.asp")) { 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("www.cbr.ru"); 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; } |
Фактически, мы формируем строку запроса, которая в браузере выглядит следующим образом "http://www.cbr.ru/currency_base/D_print.asp?date_req=DD.MM.YYYY", где вместо DD, MM, YYYY нужно подставить необходимую дату. Затем мы отправляем запрос на сервер и парсируем результат, выделяя необходимую информацию. Этот пример прекрасно работает, но имеет один существенный недостаток - он зависит от структуры HTML документа, которая может быть в любой момент изменена программистами ЦБ РФ.
Теперь настало время переключить наше внимание на разработку полноценного клиент-серверного приложения. То, что оно будет делать, не так важно, более важным является то, как оно это будет делать. Поэтому в качестве примера возьмём простой калькулятор, точнее даже умножитель. Вот текст ASP-скрипта нашей серверной части приложения:
calcasp.zip<% @Language=JScript @CODEPAGE=1251 %> <%if (Request.Form.Count) {%> <calc> <x><%= Request.Form("x") %></x> <y><%= Request.Form("y") %></y> <z><%= Request.Form("x") * Request.Form("y") %></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.
Теперь займёмся клиентской частью нашего приложения. Передача данных на сервер производится методом "PUT" или "POST". У нас уже есть функция 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("/article/inet/wininet/calc.asp",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("www.rsdn.ru"); 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, но на этот раз первая таблица будет заполнена именами полей и значениями формы ввода предыдущей страницы, т.е. Вы должны получить примерно следующее:
Form Variable | Value |
Firm | 123 |
Urbanization | |
Delivery Address | 123 |
City | 123 |
Submit | Process |
State | 12 |
Zip Code | 123 |
Теперь мы имеем полную картину, включая имена обычных и скрытых полей и их значения.
Воспользуемся полученной информацией для ответа на этот любопытный вопрос.
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(20*1024); // 1 if (rd.Post("/cgi-bin/zip4/zip4inq2", // 2 "Firm=microsoft&" // 3, 4 "Urbanization=&" "Delivery%20Address=&" // 5 "City=redmond&" "State=wa&" "Zip%20Code=&" "Submit=Process")) // 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; } |
Запустите эту программу и Вы узнаете всё необходимое. Я не буду комментировать результаты запроса, прошу Вас лишь обратить внимание на название улицы и округа :)
Комментариями отмечены места, которые нам необходимо рассмотреть для понимания происходящего.
Если Вы заметили, порядок следования полей в нашем запросе и в списке полей, выданных скриптом var.asp отличаются. Если следовать последнему, то сервер вернёт страницу с информацией о том, что имя штата, которое мы ввели не найдено в базе данных. Скрипт ASP выдаёт список полей формы именно в таком неверном порядке. С другой стороны, судя по сообщению, на сервере вообще не используются имена полей, а только их нумерация. |
К сожалению, WinInet имеет ряд ограничений, затрудняющих его использование. Подробнее об этом можно узнать в следующих статьях базы знаний Microsoft:
Обойти эти ограничения можно, если создавать только одно соединение в одном процессе и запускать этот процесс под несистемным аккаунтом. В частности, совсем не сложно создать COM объект как локальный сервер, поместить в него всю работу с WinInet и для каждого создаваемого объекта запускать отдельный экземпляр приложения, предварительно установив соответствующие настройки Identity в DCOM Config. Этот способ будет работать, но вряд ли его назовёшь изящным.
И, тем не менее, WinInet хорошо подходит для простых и средней сложности задач. Если Вам нужно добавить в программу, например, возможность online-регистрации, то, я надеюсь, теперь Вам понадобится для написания самой коммуникационной части не более получаса. Чтение WWW-страниц, как Вы могли убедиться, тоже не представляет никакой сложности. Фактически, программно Вы можете сделать всё, что Вы можете делать в браузере, включая процесс входа по паролю. Но, если Вы решите написать сам браузер... то, видимо, для этого лучше подойдут сокеты.
Happy coding,
Игорь.
Сообщений 27 Оценка 490 [+2/-0] Оценить |