3. Взаимодействие с сервером WWW

Сервера WWW предоставляют пользователям возможность просмотра гипертекстовой информации. Вся информация, расположенная на сервере, разделена на небольшие страницы.

Когда вы соединяетесь с сервером WWW с помощью какой-нибудь программы навигатора, например Microsoft Internet Explorer или Netscape Navigator, то вы увидите, что страницы могут содержать не только форматированный текст и гипертекстовые ссылки, но также рисунки, аудио и видео информацию. Некоторые страницы даже могут содержать органы управления диалоговых панелей - поля ввода, кнопки, переключатели.

Как же удается сочетать столь разнообразную информацию в одном документе? Оказывается сами страницы, адрес которых вы указываете навигатору, содержат описание на языке разметки гипертекста HTML (Hyper Text Markup Language) и хранятся в файлах с расширениями htm или html. Навигатор загружает файл страницы, а затем интерпретирует его и отображает на экране. При этом он, возможно, будет загружать и другие файлы с сервера, например файлы изображений или анимации.

В листинге 3.1 представлен пример страницы WWW, содержащей в себе текст, изображение и ссылки на другие страницы WWW (это главная рускоязычная страница нашего сервера WWW).

Листинг 3.1. Файл main.htm

<HTML>
<BODY BGCOLOR="#FFFFFF">
<BGSOUND SRC="../midi/bach.mid"></BGSOUND>
<!-- <embed src="../midi/bach.mid" width=2 height=2 hidden="yes" 
   autostart="true" loop=1> -->
<H2>Добро пожаловать на наш сервер WEB!</H2>
<P>На этом сервере вы можете найти подробную информацию
   о наших книгах из серий "Персональный компьютер. 
   Шаг за шагом" и "Библиотека системного программиста":
<UL>
<LI>аннотации и оглавления;
<LI>исходные тексты программ, опубликованных в книгах;
<LI>наши планы на будущее;
<LI>список ошибок, замеченных в наших книгах;
<LI>информацию о наших друзьях;
<LI>бесплатные книги, доступные в режиме online
</UL>
Эти серии книг предназначенны для самого широкого
круга людей, чья работа связана с компьютерами.
<P>Мы подготовили для вас и кое-что еще!
<CENTER><HR>

Эту страницу лучше всего просматривать навигаторами MS Internet Explorer v3.0 или Netscape Navigator

<TABLE><TR><TD>
<A HREF="http://www.microsoft.com/ie"><IMG SRC="ieanim.GIF" 
   ALT="MS Internet Explorer v3.0" ALIGN=right BORDER=0></A>
</TD><TD>
<A HREF="http://home.netscape.com"><IMG SRC="netnow3.GIF
   " ALT="Netscape Navigator" ALIGN=right BORDER=0></A>
</TD></TR></TABLE>
<P><IMG SRC="http://www.dials.ccas.ru/scripts/w3count.exe?frolov1" 
   ALIGN=bottom><BR>
<FONT SIZE=2>Посылайте ваши комментарии по адресу 
   <A HREF="mailto:frolov@glas.apc.org" >frolov@glas.apc.org</A></FONT> 
<BR><FONT SIZE=1>&copy; Александр Фролов, Григорий Фролов, 1997</FONT>
</CENTER>
</BODY>
</HTML>

Как видите, исходный текст страницы WWW, хранящийся на сервере, имеет мало общего с изображением этой странице в окне навигатора. Чтобы вывести страницу на экран, навигатор проделывает большую работу. Он загружает все необходимые файлы изображений, аудио и видео, которые включены в данную страницу, а затем отображает страницу на экране, руководствуясь операторами языка HTML.

Самостоятельная обработка исходных файлов HTML достаточно проста, хотя и требует очень большой и кропотливой работы, так как по сути, вам будет нужно создать интерпретатор языка HTML. Чтобы не загромождать исходные тексты наших приложений, мы не будем обрабатывать полученные страницы WWW, а основное внимание уделим процедуре взаимодействия с сервером.

Если ваше приложение должно загружать с сервера страницы WWW и отображать их на экране, не выполняя дополнительной обработки, вы можете воспользоваться органом управления Microsoft Web Browser Control. Мы расскажем про этот орган управления в главе “Ваш собственный Internet Explorer”.

Сначала необходимо инициализировать сеанс связи с Internet. Для этого надо создать объект класса CInternetSession, или класса наследованного от CInternetSession.

Затем вы должны открыть соединение с сервером WWW. Для этого надо вызвать метод GetHttpConnection класса CInternetSession. Методу GetHttpConnection указывается адрес сервера WWW. Он создает объект класса CHttpConnection. Этот объект будет представлять в вашем приложении указанный сервер WWW.

В отличие от метода GetFtpConnection класса CInternetSession, рассмотренного нами ранее, GetHttpConnection на самом деле не выполняет настоящего соединения с сервером. Поэтому даже если указать ему имя несуществующего сервера, он создаст объект класса CHttpConnection без сообщения об ошибке. Настоящее соединение с сервером будет установлено значительно позже, во время передачи серверу WWW запроса.

Теперь надо создать (открыть) и подготовить запрос для передачи серверу. Для этого вызвите метод OpenRequest класса CHttpConnection. Он создаст объект класса CHttpFile. Вы можете добавить к созданному запросу дополнительные заголовки. Для этого надо воспользоваться методом AddRequestHeaders класса CHttpFile.

Когда запрос полностью сформирован, его следует передать серверу с помощью метода SendRequest класса CHttpFile. Этот метод передает запрос указанному серверу. Именно в это время устанавливается сетевое соединение. Если запрос передан успешно, тогда метод SendRequest возвращает значение TRUE. Если указанный сервер не существует или на нем отсутствует запрашиваемый файл - возвращается значение FALSE и может быть вызвано исключение CInternetException.

Далее следует получить ответ на запрос от сервера. Для этого вызовите метод QueryInfo класса CHttpFile. В некоторых случаях, когда надо только проверить существует ли страница с указанным адресом этого уже достаточно.

Если же вам необходимо загрузить страницу с сервера на локальный компьютер, вы можете воспользоваться методом Read. Этот метод определен в классе CInternetFile, который является базовым для класса CHttpFile.

Если во время чтения файла прервется связь с сервером или произойдут другие непредвиденные события, то метод Read может вызвать исключение CInternetException. Чтобы приложение правильно обрабатывало ошибки WinInet, вы должны поместить все методы, которые могут вызвать исключение в блок try и определить для него соответствующий блок catch.

В следующем разделе мы предложим вашему вниманию приложение ConsoleHTTP, которое выполняет соединение с указанным сервером WWW. Используйте исходный текст этого приложения как шаблон для изучения основных приемов работы с серверами WWW.

Приложение ConsoleHTTP

В разделе “Приложение Console FTP“ мы привели приложение ConsoleFTP, которое соединяется с заданным сервером FTP. Для этого в нем сначала создается объект класса CInternetSession, представляющий сеанс работы с WinInet, а затем с помощью метода GetFtpConnection предпринимается попытка соединиться с сервером по заданному адресу. Если установить соединение не удается, например, из - за того, что адрес с таким адресом не существует, то метод GetFtpConnection вызывает исключение CInternetException:

// Инициализируем сеанс работы с WinInet
CInternetSession* m_InternetSession = NULL;
m_InternetSession = new CInternetSession("Connecter");   

try
{
   // Пытаемся соединиться с FTP - сервером sServer
   CFtpConnection*  m_FtpConnection = NULL;
         
   m_FtpConnection = 
         m_InternetSession -> GetFtpConnection( sServer );
   . . .
}
catch (CInternetException* pEx)
{
      // Обрабатываем исключение CInternetException
      . . . 
}

Таким образом, о существовании FTP сервера sServer можно судить по тому, вызвал ли метод GetFtpConnection исключение CInternetException. Если исключение не появилось и метод GetFtpConnection вернул не нулевое значение, значит сервер FTP существует и доступен. А если исключение все же было вызвано, значит, скорее всего, сервера с заданным именем просто не существует.

Как вы знаете, класс CInternetSession содержит в себе три метода для соединения с серверами FTP, WWW и Gopher. Это методы GetFtpConnection, GetHttpConnection и GetGopherConnection. Казалось бы достаточно заменить в исходных текстах приложения FtpConnection вызов метода GetFtpConnection на GetHttpConnection и вы сможете использовать это же приложения для поиска серверов WWW.

Проведите небольшой эксперимент и внесите в исходные тексты проекта FtpConnection соответствующие изменения:

// Инициализируем сеанс работы с WinInet
CInternetSession* m_InternetSession = NULL;
m_InternetSession = new CInternetSession("Connecter");   

try
{
   // Пытаемся соединиться с WWW - сервером sServer
   CHttpConnection*  m_HttpConnection = NULL;
         
   m_HttpConnection = 
         m_InternetSession -> GetHttpConnection( sServer );
   . . .
}
catch (CInternetException* pEx)
{
      // Обрабатываем исключение CInternetException
      . . . 
}

Постройте проект и запустите полученное приложение. Попытайтесь соединиться с помощью приложения с различными серверами WWW. Очень скоро вы обнаружите что соединение выполняется “успешно” даже с несуществующими серверами.

Чтобы быть уверенным, что сервер WWW по данному адресу не существует или недоступен, вы можете попытаться связаться с ним при помощи любой программы навигатора, например с помощью Microsoft Internet Explorer или Netscape Navigator.

Так, если вы запустите навигатор Microsoft Internet Explorer и попробуете просмотреть с его помощью сервер FTP по адресу http://home.frolov.com/, то скорее всего, получите предупреждающее сообщение, показанное нами на рисунке 3.1. Увы, пока сервера FTP с таким именем не существует.

Рис. 3.1. Сервер http://home.frolov.com/ не найден

Изменим теперь приложение ConsoleFtp так, чтобы оно не только соединялось с сервером WWW, но и запрашивало с него информацию об определенном файле. Создайте новый проект ConsoleHttp таким же образом, как вы создавали проект ConsoleFtp. Укажите что приложение будет использовать библиотеку классов MFC.

Затем создайте новый текстовый файл ConsoleHttp.cpp, наберите в нем программный код, представленный в листинге 3.2 и включите его в проект. Чтобы ускорить набор файла ConsoleHttp.cpp за его основу вы можете взять файл ConsoleFtp.cpp из проекта ConsoleFtp.

Листинг 3.2. Файл ConsoleHttp.cpp

//============================================================
// Приложение ConsoleFtp. Выполняет соединение с заданным 
// сервером WWW 
//
// (C) Фролов Г.В., 1997
// E-mail: frolov@glas.apc.org
// WWW:    http://www.glasnet.ru/~frolov
//         или
//         http://www.dials.ccas.ru/frolov
//============================================================

// Включаемый файл для библиотеки MFC
#include <afx.h>
// Включаемый файл для классов WinInet
#include <afxinet.h>
// Включаемый файл для консольного ввода/вывода
#include <iostream.h>
// Включаемый файл для стандартных функций
#include <stdlib.h>

//============================================================
// Главная функция приложения
//============================================================
int main(int argc, char* argv[])
{
   // Если приложение запущено без параметра, отображаем на 
   // экране формат вызова приложения
   if (argc != 2)
   {
      cout << "Programm format: ConsoleFtp <URL>" << endl;
      cout << "  <URL> - URL address of WWW" << endl << endl;
      return -1;
   }

   // Записываем параметр, указанный при вызове приложения в 
   // текстовую строку sUrlAdress. Он должен содержать URL 
   // адрес сервера WWW 
   CString sUrlAdress;
   sUrlAdress = argv[1];
   
   // Отображаем адрес URL на экране
   cout << "URL address: " << sUrlAdress << endl << endl;

   // Определяем переменные, в которые будут записаны 
   // отдельные компоненты адреса, указанного пользователем.
   // Имя сервера
   CString sServer;    
   // Имя объекта на который указывает адрес URL 
   CString sObject;
   // Номер порта
   INTERNET_PORT nPort;
   // Тип сервиса или тип протокола
   DWORD dwServiceType;
   
   // Разбираем адрес URL, записанный в строке sUrlAdress
   if (!AfxParseURL(sUrlAdress, dwServiceType, sServer, 
                    sObject, nPort))
   {
      // В случае ошибки выводим сообщение и завершаем 
      // работу приложения
      cout << "AfxParseURL Error" << endl;
      
      return -1;
   }

   // Проверяем, соответствует ли указанный адрес URL 
   // серверу WWW. Для этого тип сервиса должен быть http://
   if(dwServiceType != AFX_INET_SERVICE_HTTP)
   {
         cout << "URL Address not WWW server" << endl;
         return -2;
   }

   // Указатель на объект класса CInternetSession
   CInternetSession* m_InternetSession = NULL;

   // Инициализируем сеанс работы с WinInet - создаем объект 
   // класса CInternetSession 
   m_InternetSession = new CInternetSession("Connecter");   

   // Запрос для сервера WWW       
   CHttpFile* pFile=NULL;   

   // Определяем указатель на объект класса CFtpConnection
   CHttpConnection*  m_HttpConnection = NULL;

   // Исключения, вызванные в этом блоке try обрабатываются 
   // следующим блоком catch
   try
   {
      // Пытаемся соединииться с сервером sServer
      m_HttpConnection = 
         m_InternetSession -> GetHttpConnection( sServer );
      // Формируем запрос
      pFile = 
         m_HttpConnection -> OpenRequest(
                              CHttpConnection::HTTP_VERB_HEAD,
                              sObject
                             );

      // Дополнительный заголовок запроса HTTP 
      CString headerInfo(_T(
         "Accept: text/*\r\nUser-Agent: HTTP Example\r\n"));
   
      // Добавляем к запросу дополнительный заголовок
      if(pFile->AddRequestHeaders(headerInfo))
      {
         // Передаем запрос на сервер
         if(pFile->SendRequest())
         {
            DWORD dwReturnCode;

            // Определяем код завершения запроса
            if(pFile->QueryInfoStatusCode(dwReturnCode))
            {
               // Отображаем код завершения запроса на экране
               CString sMessage;
               sMessage.Format("%d",dwReturnCode);
               cout << "SendRequest: " << sMessage << 
                        endl << endl;
            }
            else
            {
               cout << "Request Error" << endl << endl;
            }
         }
      }
   
   }
   // Обрабатываем исключение CInternetException
   catch (CInternetException* pEx)
   {
      // Временный буфер для сообщения
      TCHAR szErr[1024];  

      // Выводим сообщение об ошибке
      if (pEx->GetErrorMessage(szErr, 1024))
         cout << "Error: " << szErr <<endl <<endl;

      // Удаляем иссключение
      pEx->Delete();
   }

   // Закрываем запрос
   pFile -> Close();
   // Удаляем объект pFile из памяти
   if(pFile)
      delete pFile;

   // Закрываем соединение с сервером WWW 
   m_HttpConnection -> Close();

   // Удаляем объект m_FtpConnection
   delete( m_HttpConnection );

   // Завершаем сеанс связи
   m_InternetSession -> Close();

   // Удаляем объект m_InternetSession
   delete(m_InternetSession);

   return 0;
}

Постройте только что созданный проект и запустите приложение, указав ему в качестве параметра адрес WWW страницы. Адрес должен быть полным - он должен определять метод доступа (в данном случае http://), имя сервера на котором страница расположена и полный путь к файлу страницы WWW.

Приложение соединится с сервером, на котором эта страница расположена и запросит о ней информацию. Результаты запроса будут выведены на экран. На рисунке 3.3 мы показали работу приложения ConsoleHttp, запросив с сервера http://dials.ccas.ru/ страницу с именем frolov.htm.

Рис. 3.2. Приложение ConsoleHttp запрашивает страницы http://www.frolov.ru/~frolov и http://www.glasnet.ru/~frolov

Устройство приложения ConsoleHttp

Рассмотрим теперь устройство приложения ConsoleHttp. Первая часть приложения ConsoleHttp практически совпадает с приложением ConsoleFtp, работа которого была рассмотрена нами выше в разделе “Устройство приложения ConsoleFtp”. Поэтому мы не будем на нем останавливаться. Скажем только что функция main сначала проверяет командную строку приложения. Если приложение было вызвано без параметров, или их больше чем один, то на экран выводится информация о формате вызова приложения.

Если пользователь правильно указал в командной строке ConsoleHttp только один параметр, он записывается в строку sUrlAdress и отображается на экране:

CString sUrlAdress;
sUrlAdress = argv[1];

Далее строка sUrlAdress передается для обработки функции AfxParseURL, которая разбирает адрес, записанный в ней на составные части и помещает их в переменные dwServiceType, sServer, sObject и nPort:

DWORD dwServiceType; // Тип сервиса
CString sServer;     // Имя сервера
CString sObject;     // Имя объекта на который указывает адрес
INTERNET_PORT nPort; // Номер порта 

if(!AfxParseURL( sUrlAdress, dwServiceType, 
                 sServer, sObject, nPort))
{
   cout << "AfxParseURL Error" << endl;
   return -1;
}

В том случае если пользователь указал в командной строке приложения неправильный адрес, не соответствующий формату URL, то функция AfxParseURL возвращает нулевое значение. Наше приложение отображает сообщение об ошибке и завершается с кодом -1.

Далее приложение проверяет, что указанный адрес соответствует адресу сервера WWW. В этом случае тип сервиса dwServiceType должен быть равен значению AFX_INET_SERVICE_HTTP. Если это не так, например, вместо адреса сервера WWW вы указали адрес сервера FTP, который начинается с ftp://, то на экран будет выведено сообщение об ошибке и приложение завершится с кодом возврата -2:

if(dwServiceType != AFX_INET_SERVICE_HTTP)
{
   cout << "URL Address not WWW server" << endl;
   return -2;
}

Если адрес сервера указан верно, приложение создает сеанс связи с Internet. Для этого мы объявляем указатель на объект класса CInternetSession а затем создаем сам объект, используя функцию new:

// Указатель на объект класса CInternetSession
CInternetSession* m_InternetSession = NULL;

// Инициализируем сеанс работы с WinInet - создаем объект 
// класса CInternetSession catch
m_InternetSession = new CInternetSession("WWW Connecter");   

Конструктор класса CInternetSession имеет много параметров, но мы все их используем по умолчанию, кроме первого параметра, через который мы задаем имя сеанса - WWW Connecter.

Теперь можно попытаться соединиться с сервером и запросить с него файл. Для этого следует вызвать метод GetHttpConnection класса CInternetSession только что созданного объекта m_InternetSession. В случае ошибки этот метод может вызвать исключение CInternetException. Поэтому вызов метода GetHttpConnection мы помещаем в блок try и определяем для него соответствующий блок catch, обрабатывающий исключение CInternetException:

// Исключения, вызванные в этом блоке try обрабатываются 
// следующим блоком catch
try
{
   // . . .
}
catch (CInternetException* pEx)
{
   // . . .
}

Обработчик исключения CInternetException вызывает метод GetErrorMessage для полученного исключения, который записывает во временный буфер szErr причину вызова исключения. Если текстовое описание исключения доступно, метод GetErrorMessage возвращает ненулевое значение. В этом случае мы отображаем полученный текст на экране.

Затем мы завершаем обработку исключения, вызывая для него метод Delete. Он удаляет объект, представляющий исключение из оперативной памяти:

// Удаляем исключение
pEx->Delete();

Рассмотрим блоку try более подробно. В нем приложение исполняет свою главную задачу - пытается установить соединение с сервером WWW. Адрес сервера WWW передается методу GetHttpConnection класса CInternetSession. Если соединение с сервером будет установлено, то этот метод вернет указатель на объект класса GetHttpConnection и мы запишем его во временную переменную m_HttpConnection:

// Определяем указатель на объект класса CHttpConnection
CHttpConnection*  m_HttpConnection = NULL;
// Пытаемся соединиться с сервером sServer
m_HttpConnection = 
   m_InternetSession -> GetHttpConnection( sServer );

Далее приложение HttpConnection вызывает для установленного соединения метод OpenRequest. Первый параметр метода OpenRequest определяет действие. В данном случае в качестве этого параметра используется константа HTTP_VERB_HEAD, означающая что мы желаем только получить информацию о объекте указанном во втором параметре метода sObject:

CHttpFile* pFile=NULL;   
. . .
pFile = m_HttpConnection -> 
   OpenRequest( CHttpConnection::HTTP_VERB_HEAD, 
                sObject 
              );

Если метод OpenRequest завершился успешно, он возвращает указатель на объект класса CHttpFile. Далее вы можете использовать методы класса CHttpFile чтобы узнать данные полученные с сервера. Если вам надо получить с сервера файл, вы должны использовать вместо команды HTTP_VERB_HEAD команду HTTP_VERB_GET.

Сначала для объекта pFile вызывается метод AddRequestHeaders. Он добавляет дополнительный заголовок headerInfo к запросу HTTP. Сам заголовок headerInfo содержит текстовую строку, в которой определяются тип файлов, которые может принимать наше приложение и его имя:

CString headerInfo( 
    _T("Accept: text/*\r\nUser-Agent: Console Http\r\n"));

if(pFile->AddRequestHeaders(headerInfo))
{
}

Если метод AddRequestHeaders завершился без ошибок, он возвращает ненулевое значение. В этом случае наше приложение вызывает метод SendRequest, который и передает сформированный запрос серверу WWW. Если запрос передан успешно, то метод SendRequest возвращает ненулевое значение. В противном случае возвращается нуль. В случае возникновения ошибок метод SendRequest может вызвать исключение CInternetException:

if(pFile->SendRequest())
{
}

Обратите внимание, что исключение CInternetException может также быть вызвано методом GetHttpConnection. Мы поместили эти методы в один блок try, поэтому исключение вызванное любым из них будет обработано одним и тем же блоком catch.

Метод SendRequest только передает запрос серверу. Чтобы получить от него ответ, мы обращаемся к методу QueryInfoStatusCode. Этот метод получает код состояния, связанный с последним запросом к серверу и записывает его во временную переменную dwReturnCode.

Если метод QueryInfoStatusCode отработал успешно, он возвращает ненулевое значение. В этом случае мы преобразуем значение dwReturnCode в текстовую форму и отображаем его на экране:

DWORD dwReturnCode;
if(pFile -> QueryInfoStatusCode(dwReturnCode))
{
   CString sMessage;
   sMessage.Format("%d",dwReturnCode);

   cout << "SendRequest: " << sMessage << endl << endl;
}

Больше приложение ConsoleHttp ничего не делает с сервером, поэтому закрываем объект pFile и удаляем его из памяти:

pFile->Close();
if(pFile)
   delete pFile;

Затем закрываем соединение с сервером и удаляем соответствующий объект m_HttpConnection из памяти (этот объект создается методом GetHttpConnection):

// Закрываем соединение с сервером WWW 
m_HttpConnection -> Close();
// Удаляем объект m_HttpConnection
delete( m_HttpConnection );

Перед окончанием работы приложения, завершаем сеанс связи, вызывая метод Close класса CInternetSession, а затем удаляем соответствующий объект из памяти, вызывая функцию delete:

// Завершаем сеанс связи
m_InternetSession -> Close();
// Удаляем объект m_InternetSession
delete(m_InternetSession);