Электронная библиотека книг Александра Фролова и Григория Фролова.
Shop2You.ru Создайте свой интернет-магазин
Библиотека
Братьев
Фроловых
[Назад] [Содержание] [Дальше]

Создание приложений с базами данных для Интернета и интрасетей: практическое руководство

(С) Александр Вячеславович Фролов, Григорий Вячеславович Фролов, 2000

7. Расширения CGI и ISAPI сервера Web

7. Расширения CGI и ISAPI сервера Web.. 1

Программы CGI и базы данных.. 2

Немного о формах HTML. 2

Передача данных программе CGI 3

Метод GET. 3

Метод POST. 4

Выбор между GET и POST. 4

Передача ответа из программы CGI 5

Переменные среды для программы CGI 5

Примеры программ CGI 7

Программа CGIHELLO.. 7

Программа CONTROLS. 8

Программа AREF. 13

Доступ к базе данных из CGI 14

Создание приложений ISAPI 20

Принципы работы и структура расширения ISAPI 20

Вызов расширения ISAPI сервером Web. 21

Функция GetExtensionVersion. 21

Функция HttpExtensionProc. 21

Получение данных расширением ISAPI 24

Функция GetServerVariable. 24

Функция ReadClient. 26

Отправка данных расширением ISAPI 27

Функция WriteCilent. 27

Функция ServerSupportFunction. 27

Приложение ISHELLO.. 28

Вызов функций ODBC из ISAPI 31

Обращение к базе данных в отдельном потоке. 36

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

Однако в некоторых случаях Вам придется создавать собственные расширения сервера Web в виде программ CGI и приложений ISAPI. Как правило, такие расширения, работающие на том же сервере, что и сервис Web, необходимы для обращения к нестандартным интерфейсам, например к интерфейсам платежных систем компаний, занимающихся процессингом кредитных карточек через Интернет.

Если процессинговая компания предоставляет в Ваше распоряжение интерфейсный модуль в виде объекта компонентной модели Microsoft COM, к нему можно обращаться непосредственно из серверного сценария, расположенного на страницах ASP. Однако чаще всего модуль поставляется в виде библиотеки динамической загрузки DLL, экспортирующей набор функций. Такие функции легко вызываются из программ, составленных на языке C, но они недоступны программам серверного сценария JScript или VB Script, встроенного в страницы ASP.

Необходимо отметить, что, если Вы по каким-либо причинам не можете или не желаете применять технологию ASP, Вы вполне обойдетесь и без нее. К Вашим услугам приложения с базами данных для Интернета или интрасетей, разработанные с использованием одних только расширений сервера Web. Однако этот путь представляется нам намного более трудным, так как он предполагает программирование на уровне вызова функций программного интерфейса Win32. К тому же программы расширений намного сложнее в отладке по сравнению с серверными сценариями ASP.

В этой главе мы расскажем Вам о том, как создать собственные расширения сервера Web, созданного на базе Microsoft Internet Information Server версии 4.0, в виде программ CGI и приложений ISAPI.

Так как наша книга посвящена базам данных, то мы рассмотрим вопросы интеграции приложений CGI и ISAPI с сервером базы данных Microsoft SQL Server.

Программы CGI и базы данных

Если Вы хотя бы чуть-чуть занимались разработкой приложений для Интернета, Вы наверняка слышали о программах CGI. Тем не менее мы позволим себе немного рассказать о них.

Что кроется за аббревиатурой CGI?

CGI — это стандартный шлюзовой интерфейс (Common Gateway Interface) для запуска внешних программ под управлением сервера Web. Соответственно приложениями CGI называются программы, которые, пользуясь этим интерфейсом, получают через протокол HTTP информацию от удаленного пользователя, обрабатывают ее и возвращают результат обработки обратно в виде ссылки на уже существующий документ HTML или другой объект (например, графическое изображение) или в виде документа HTML, созданного динамически.

Передача информации от удаленного пользователя приложению CGI обычно выполняется следующим образом.

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

Когда пользователь заполняет форму и щелкает указанную кнопку, данные передаются приложению CGI, путь к которому задается в заголовке формы. Это приложение получает через протокол HTTP данные из полей формы в виде «имя поля/значение».

После обработки полученных данных приложение CGI создает документ HTML и записывает его в стандартное устройство вывода stdout. Этот документ затем автоматически передается удаленному пользователю.

Так как приложение CGI представляет собой не что иное, как программу, Вы должны оттранслировать ее для той операционной системы, под управлением которой работает Ваш сервер Web. В нашем случае необходимо создать консольное приложение Win32 (не путайте его с консольной программой MS-DOS — это разные вещи).

Заметим, что программы CGI, создаваемые для операционной системы Unix, часто составляют с применением интерпретируемого языка Perl. Однако наша книга ориентирована на применение технологий Microsoft, поэтому мы не будем касаться этой темы. Заметим только, что для платформы Microsoft Windows NT также существуют реализации интерпретатора языка Perl.

Немного о формах HTML

Чаще всего программы CGI (и приложения ISAPI, которые мы рассмотрим позже) применяются для обработки данных, введенных посетителями сервера Web при помощи форм. Хотя Вы уже имеете некоторый опыт в создании форм, мы рассмотрим некоторые вопросы, касающиеся взаимодействия форм и расширений сервера Web.

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

Для того чтобы сделать форму в документе HTML, следует воспользоваться тегом <FORM>. Этот тег применяется в паре с тегом </FORM>, завершающим описание формы. Между тегами <FORM> и </FORM> находятся описания элементов управления в виде таких тегов, как <INPUT>, <TEXTAREA> и <SELECT> с соответствующими параметрами.

Вот пример определения простейшей формы:

<FORM METHOD=GET ACTION="http://www.myserver.ru/cgi/form.exe">
  <TABLE>
  <TR>
     <TD><INPUT TYPE=text NAME="text1" VALUE="Sample of text1"></TD>
  </TR>
  <TR>
     <TD><INPUT TYPE=text NAME="text2" VALUE="Sample of text2"></TD>
  </TR>
  <TR>
     <INPUT TYPE=submit VALUE="Send">
  </TR>
  </TABLE>
</FORM>

Здесь элементы управления размещаются в таблице, состоящей из одного столбца и трех строк. В верхних двух строках мы расположили поля ввода и редактирования текста, а в последней строке — кнопку Send.

Назначение параметров тега  <FORM> описаны в таблице 7-1.

Таблица 7-1. Параметры тега <FORM>

Параметр

Описание

ACTION

Адрес URL для выполнения действий над результатами, введенными в элементах формы

METHOD

Способ передачи данных из формы на сервер Web

ENCTYPE

Тип MIME передаваемых данных

Параметр ACTION определяет, какое действие будет выполнено над формой, после того как пользователь ее заполнит и передаст серверу Web. В примере, приведенном выше, в качестве значения для параметра ACTION мы указали путь к программе CGI, которая будет выполнять обработку данных.

Посредством параметра METHOD Вы можете выбрать один из двух методов передачи данных из формы серверу Web.

Если значение этого параметра равно GET (как в нашем примере), программа CGI, указанная в параметре ACTION, получит данные из формы через переменную среды с именем QUERY_STRING. В том случае, когда значение параметра METHOD равно POST, программа CGI получит данные из формы через стандартный поток ввода. Позже мы рассмотрим различия этих методов более подробно.

И наконец, третий параметр ENCTYPE, используется очень редко и только для метода POST. Он позволяет указать тип передаваемых данных и по умолчанию имеет значение application/x-www-form-urlencoded.

Заметим, что для отправки формы на сервер Web можно использовать графическую кнопку типа IMAGE. Изображение такой кнопки задается параметром SRC.

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

Передача данных программе CGI

Когда пользователь заполняет форму и щелкает кнопку типа SUBMIT либо графическую кнопку (которая выполняет аналогичную функцию), данные из полей формы вместе с именами этих полей передаются браузером серверу Web. Тот, в свою очередь, анализирует эти данные и запускает соответствующую программу CGI, путь к файлу которой указан в теге <FORM>.

Перед запуском программы CGI сервер Web выбирает в зависимости от значения параметра METHOD тега <FORM> один из двух способов передачи полученных данных для обработки — метод GET или POST.

Метод GET

Метод GET предполагает передачу данных программе CGI через переменные среды (environment variable). Это те самые переменные среды, которые устанавливаются в операционной системе MS-DOS командой SET.

Сервер Web создает для программы CGI довольно много переменных среды. Имена и назначение всех этих переменных Вы узнаете позже, а пока мы расскажем только о самых необходимых.

Прежде всего, метод GET предполагает использование переменной среды с именем QUERY_STRING. Именно сюда попадают данные из полей формы. Эти данные находятся в следующем формате:

“Имя1=Значение1&Имя2=Значение2&Имя3=Значение3”

Здесь в качестве имен используются значения параметров NAME, задающих имена полей формы. Вместо значений подставляются данные из соответствующих полей.

Сканируя содержимое текстовой строки переменной среды QUERY_STRING, программа CGI найдет в ней имя любого нужного поля и соответствующее этому имени значение. Заметим, что если переключатель не отмечен, то никакие данные от него не передаются. Поэтому не следует думать, что в полученной строке Вы обязательно встретите имена всех полей, расположенных в форме.

Адрес строки любой переменной среды в программе, составленной на C, легко получить с помощью функции getenv:

char * szQueryString;
szQueryString = getenv("QUERY_STRING");

Заметим, что если Вы собираетесь модифицировать строку переменной среды, то ее следует скопировать во внутренний буфер. Операционная система сервера Web иногда не допускает прямого редактирования блока памяти, содержащего переменные среды.

Строка, передаваемая в переменной среды QUERY_STRING, закодирована с использованием так называемой кодировки URL. В этом случае все символы пробелов заменяют символами «+». Кроме того, для представления кодов управляющих и некоторых других символов используется последовательность символов вида «%xx», где «xx» — это шестнадцатеричный код символа в виде двух символов ASCII. В нашей книге мы приведем исходные тексты функций, предназначенных для перекодирования информации, полученной из форм.

Метод POST

При использовании метода POST программа CGI получает данные из формы через стандартный поток ввода stdin. Если программа CGI составлена на языке программирования C, то для получения данных она может воспользоваться такими функциями, как fread или scanf.

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

Ниже мы приводим фрагмент кода для определения размера информации для ввода через стандартный поток stdin:

int Size;
Size = atoi(getenv("CONTENT_LENGTH"));

Входные данные можно затем получить, например, следующим образом:

char szBuf[8196];
fread(szBuf, Size, 1, stdin);

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

Если в теге <FORM> не указан параметр ENCTYPE (тип MIME передаваемых данных) или этот параметр имеет значение application/x-www-form-urlencoded, данные, полученные через стандартный поток ввода, закодированы в URL. Перед использованием Вы должны их перекодировать соответствующим образом.

Выбор между GET и POST

Метод GET обычно применяют для обработки небольших форм, так как браузеры и операционные системы накладывают ограничения на размер данных, передаваемых через переменную среды QUERY_STRING.

В этом отношении метод POST предпочтительнее, так как не ограничивает размер передаваемых данных.

Передача ответа из программы CGI

Вне зависимости от метода передачи данных (GET или POST) результат своей работы программа CGI направляет в стандартный поток вывода stdout. Если программа составлена на языке программирования C, для записи результат работы она может воспользоваться, например, функцией printf или fwrite.

Чаще всего программы CGI применяют для создания динамических документов HTML на основе данных, полученных из формы. В этом случае первой строкой, которую необходимо вывести в стандартный поток вывода stdout, будет следующая строка заголовка HTTP:

Content-type: text/html

Сразу за ней необходимо вывести еще одну пустую строку, которая послужит разделителем между заголовком HTTP и данными документа HTML.

Ниже мы привели фрагмент кода, в котором программа CGI динамически формирует документ HTML и выводит его в стандартный поток вывода:

printf("Content-type: text/html\n\n");
printf("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2//EN\">");
printf("<HTML><HEAD><TITLE>XYZ Incorporation</TITLE></HEAD>”
  ”<BODY BGCOLOR=#FFFFFF>");
printf("<H1>Результаты обработки формы</H1>");
. . .
printf("</BODY></HTML>");

Обратите внимание на символы перевода строки «\n\n». Первый из них закрывает строку заголовка HTTP, а второй нужен для создания пустой разделительной строки.

Переменные среды для программы CGI

Прежде чем перейти к примерам программ CGI, мы расскажем о переменных среды, которые формируются для этих программ перед запуском. Через эти переменные помимо данных из полей форм передается и другая очень важная информация, причем ее не всегда можно игнорировать.

Рассмотрим по отдельности назначение переменных среды. Заметим, что набор переменных, создаваемых при запуске программы CGI, зависит от конкретной реализации сервера Web.

 

·    AUTH_TYPE

Технология Web допускает защиту страниц HTML, когда доступ к отдельным страницам предоставляется только для отдельных пользователей при предъявлении пароля. При этом используется система аутентификации, или проверки подлинности идентификатора пользователя.

Переменная среды AUTH_TYPE содержит тип идентификации, который применяется сервером. Например, для сервера Web типа Microsoft Information Server при включении аутентификации в этой переменной будет храниться строка «NTLM».

·    GATEWAY_INTERFACE

В этой переменной находится версия интерфейса CGI, с которой работает данный сервер.

·    HTTP_ACCEPT

В этой переменной перечислены типы данных MIME, которые могут быть приняты браузером от сервера Web. Например, сервер Microsoft Internet Information Server может передать браузеру растровые графические изображения в формате gif, jpeg, pjpeg, x-xbitmap. Подробно эти типы данных описаны в спецификации протокола MIME, рассмотрение которой выходит за рамки нашей книги.

·    HTTP_REFER

В переменную HTTP_REFER записывается адрес URL документа HTML, который инициировал работу программы CGI.

·     HTTP_ACCEPT_LANGUAGE

Переменная HTTP_ACCEPT_LANGUAGE содержит идентификатор предпочтительного национального языка для получения ответа от сервера Web.

·     HTTP_UA_PIXELS

Разрешение видеоадаптера, установленное в компьютере пользователя.

·    HTTP_UA_COLOR

Допустимое количество цветов в системе пользователя.

·    HTTP_UA_OS

Операционная система, под управлением которой работает браузер.

·    HTTP_UA_CPU

Тип центрального процессора, установленного в компьютере пользователя.

·    HTTP_USER_AGENT

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

·    HTTP_HOST

Имя узла, на котором работает сервер Web.

·    HTTP_CONNECTION

Тип соединения.

·    HTTP_ACCEPT_ENCODING

Метод кодирования, который может быть использован браузером для формирования ответа серверу Web.

·    HTTP_AUTHORIZATION

Информация авторизации от браузера. Используется браузером для собственной аутентификации в сервере Web.

·    HTTP_FROM

Имя пользователя в виде, как оно было зарегистрировано при настройке браузера. Используется формат адресов электронной почты.

·    HTTP_PRAGMA

Специальные команды серверу Web.

·    CONTENT_LENGTH

Количество байт данных, которые программа CGI должна получить от браузера.

·    CONTENT_TYPE

Тип данных, присланных браузером.

·    PATH_INFO

Путь к виртуальному каталогу, в котором находится программа CGI.

Как правило, при настройке сервера Web администратор выделяет один или несколько каталогов для хранения расширений сервера в виде программ CGI или ISAPI. Для файлов, записанных в такие каталоги, устанавливается доступ на запуск.

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

·    PATH_TRANSLATED

Физический путь к программе CGI.

·    QUERY_STRING

Строка параметров, указанная в форме после адреса URL программы CGI после разделительного символа «?».

·    REMOTE_ADDR

Адрес IP узла, на котором работает браузер пользователя.

·    REMOTE_HOST

Доменное имя узла, на котором работает браузер пользователя. Если эта информация недоступна (например, для узла не определен доменный адрес), вместо доменного имени указывается адрес IP, как в переменной REMOTE_ADDR.

·    REMOTE_USER

Имя пользователя, которое используется браузером для аутентификации. Применяется только в том случае, если сервер Web способен работать с аутентификацией, и программа CGI отмечена как защищенная.

·      REQUEST_METHOD

Метод доступа, который используется для передачи данных от браузера серверу Web. В своих примерах мы используем методы доступа GET и POST, хотя протокол HTTP допускает применение и других методов доступа, например, PUT и HEAD.

·    SCRIPT_NAME

В эту переменную записывается путь к виртуальному каталогу и имя программы CGI. Анализируя эту переменную, программа CGI может определить путь к своему загрузочному файлу.

·    SERVER_NAME

Доменное имя сервера Web или адрес IP сервера Web, если доменное имя недоступно или не определено.

·    SERVER_PROTOCOL

Имя и версия протокола, который применяется для выполнения запроса к программе CGI.

·    SERVER_PORT

Номер порта, на котором навигатор посылает запросы серверу Web.

·       SERVER_SOFTWARE

Название и версия программного обеспечения сервера Web. Версия следует после названия и отделяется от названия символом «/».

·    REMOTE_IDENT

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

Примеры программ CGI

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

Программа CGIHELLO

Программа CGIHELLO представляет собой простейшую программу CGI, которая запускается при помощи кнопки в форме, возвращая навигатору созданный динамически документ HTML.

Эта программа хороша для проверки возможности запуска программ CGI на Вашем сервере Web или на сервере Вашего поставщика услуг Интернета. Так как она очень проста, то существует немного причин, по которым она не работает. Это неправильная настройка прав доступа к виртуальному каталогу, содержащему загрузочный модуль программы CGI, а также ошибочная ссылка на этот каталог в параметре ACTION тега <FORM>.

Еще одна возможная причина — неправильное указание типа проекта в среде Visual C++. Напоминаем, что программа CGI, предназначенная для работы в среде Windows NT, должна обязательно создаваться как 32-разрядное консольное приложение, а не как консольная программа MS-DOS.

Наша первая программа CGI запускается из формы, расположенной в документе HTML с именем cgihello.html. Исходный текст этого документа представлен в листинге 7-1.

Листинг 7-1 Вы найдете в файле ch7\cgihello\cgihello.html на прилагаемом к книге компакт-диске.

В этом документе определена форма, содержащая единственную кнопку, созданную с применением тега <INPUT> и имеющую тип SUBMIT:

<form METHOD="GET" ACTION="http://saturn/cgi-bin/cgihello.exe">
  <p><input TYPE="submit" VALUE="Send"> </p>
</form>

В параметре ACTION тега <FORM> мы указали путь к программе CGI, причем этот путь является виртуальным. Для передачи данных используется метод GET.

В результате работы программы CGIHELLO динамически создается документ HTML, страница которого показан на рис. 7-1.

Рис. 7-1. Документ HTML, создаваемый динамически программой CGGIHELLO

Рассмотрим исходный текст программы CGIHELLO (листинг 7-2).

Листинг 7-2 Вы найдете в файле ch7\cgihello\cgihello.с на прилагаемом к книге компакт-диске.

Эта консольная программа не отличается особой сложностью и состоит из ряда вызовов функции printf, выполняемых функцией main:.

void main(int argc, char *argv[])
{
  printf("Content-type: text/html\n\n");
  printf("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2//EN\">");
  printf("<HTML><HEAD><TITLE>"
      "Программы CGI</TITLE></HEAD><BODY>");

  printf("<H1>Результат работы программы CGI</H1>");
  printf("<P>Эта страница создана динамически в результате"
     " работы программы CGI");
  printf("</BODY></HTML>");
}

В первый раз функция printf выводит заголовок HTTP и пустую строку-разделитель. Далее программа CGIHELLO записывает построчно в стандартный поток вывода текст документа HTML.

Программа CONTROLS

Более сложная программа CGI с названием CONTROLS выполняет обработку данных, полученных из формы, показанной на рис. 7-2.

Рис. 7-2. Поле с элементами различных типов

Исходный текст этой формы представлен в листинге 7-3.

Листинг 7-3 Вы найдете в файле ch7\controls\controls.html на прилагаемом к книге компакт-диске.

Программа CONTROLS отображает в динамически формируемом документе HTML метод, использованный для передачи (POST или GET), а также размер и тип данных, поступающих от формы. Принятые данные отображаются как в исходном виде, так и после перекодировки. Кроме того, в созданном динамически документе HTML Вы сможете увидеть список значений всех полей, определенных в форме (рис. 7-3).

Рис. 7-3. Документ HTML, сформированный динамически программой CONTROLS

Из рисунка видно, что браузер прислал серверу Web 127 байт информации. Так как при этом использовался метод POST, данные были направлены в стандартный поток ввода. Они представлены в кодировке URL, так как содержимое переменой среды CONTENT_TYPE равно application/x-www-form-urlencoded.

Обратите внимание на текстовое поле с именем text1. Все пробелы в соответствующей строке в кодировке URL заменены символом «+». Символы «&» и «,» приходят в виде «%26» и «%2C». Функция перекодирования возвращает строку в исходный вид — «Sample of text1».

Форма, показанная на рис. 7-2, имеет две кнопки, предназначенные для передачи данных серверу Web. Это обычная кнопка и кнопка в графическом виде. Мы щелкнули графическую кнопку, поэтому от формы пришла информация о координатах курсора мыши в виде переменных с именами x и y.

Исходный текст программы CONTROLS приведен в листинге 7-4.

Листинг 7-4 Вы найдете в файле ch7\controls\controls.с на прилагаемом к книге компакт-диске.

После запуска функция main программы CONTROLS выводит в стандартный поток вывода заголовок HTTP и начальные строки динамически формируемого документа HTML. Для вывода мы использовали функцию printf:

printf("Content-type: text/html\n\n");

printf("<!DOCTYPE HTML PUBLIC"
     " \"-//W3C//DTD HTML 3.2//EN\">");

printf("<HTML><HEAD><TITLE>Программы CGI"
     "</TITLE></HEAD><BODY BGCOLOR=#FFFFFF>");

szMethod = getenv("REQUEST_METHOD");

Далее функция main определяет использованный метод передачи данных, анализируя содержимое переменной среды REQUEST_METHOD:

if(!strcmp(szMethod, "POST"))
{
  . . .
}
else if(!strcmp(szMethod, "GET"))
{
  . . .
}

Это необходимо, так как при разных методах передачи нужно использовать различные методы получения входных данных. Значение переменной среды программа получает при помощи функции getenv.

Если данные передаются методом POST, программа будет считывать их из стандартного потока ввода. Размер данных находится в переменной среды CONTENT_LENGTH. Соответствующая текстовая строка получается функцией getenv и преобразуется в численное значение функцией atoi.

Чтение данных из входного потока выполняется за один вызов функции fread:

lSize = atoi(getenv("CONTENT_LENGTH"));
fread(szBuf, lSize, 1, stdin);

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

Программа CGI сохраняет принятые данные в файле для дальнейшей обработки. Наша программа создает в текущем каталоге файл с названием received.dat:

fileReceived = fopen("received.dat", "w");
fwrite(szBuf, lSize, 1, fileReceived);
fclose(fileReceived);

Текущим каталогом при запуске этой программы в среде сервера Microsoft Information Server будет каталог с загрузочным модулем программы CONTROLS. Заметим, что, для того чтобы программа CGI могла создать файл в каталоге, необходимо соответствующим образом настроить права доступа.

После сохранения принятых данных в файле программа CONTROLS выводит в стандартный поток вывода содержимое некоторых переменных среды: REQUEST_METHOD, CONTENT_LENGTH и CONTENT_TYPE:

printf("<H2>Переменные среды</H2>");
    
printf("REQUEST_METHOD = %s", getenv("REQUEST_METHOD"));
printf("<BR>CONTENT_LENGTH = %ld", lSize);
printf("<BR>CONTENT_TYPE = %s", getenv("CONTENT_TYPE"));

szBuf[lSize] = '\0';
strcpy(szSrcBuf, szBuf);

printf("<H2>Принятые данные</H2>");
printf("<P>%s", szSrcBuf);

Далее наша программа перекодирует данные, полученные в кодировке URL. Перед этим принятые данные копируются в буфер, где они будут обновляться по месту. Для копирования мы закрываем буфер двоичным нулем, после чего копирование можно выполнить функцией копирования строки strcpy.

Перекодироввание выполняется функцией DecodeStr, определенной в нашем приложении. Эту функцию мы рассмотрим позже. Результат сохраняется в буфере szSrcBuf, откуда он и берется для отображения:

DecodeStr(szSrcBuf);
printf("<H2>Данные после перекодировки</H2>");
printf("<P>%s", szSrcBuf);

На завершающем этапе обработки данных, полученных от формы, программа CONTROLS записывает в выходной документ HTML значения отдельных полей. Напомним, что эти значения имеют формат &<Имя_поля>=<Значение>, при этом символ «&» используется как разделитель.

Наша программа закрывает исходный буфер с принятыми данными дополнительным символом «&» (для простоты сканирования), после чего запускает цикл по полям формы.

szBuf[lSize] = '&';
szBuf[lSize + 1] = '\0';

for(szParam = szBuf;;)
{
  szPtr = strchr(szParam, '&');
      
  if(szPtr != NULL)
  {
     *szPtr = '\0';
     DecodeStr(szParam);
     printf("%s<BR>", szParam);

     szParam = szPtr + 1;
     if(szParam >= (szBuf + lSize))
       break;
  }
  else
     break;
}

В этом цикле во входной строке с помощью функции strchr ищется символ разделитель. Если этот символ найден, мы его заменяем на двоичный нуль, после чего полученная текстовая строка значения параметра перекодируется функцией DecodeStr и выводится в стандартный выходной поток.

Цикл завершается, когда в процессе сканирования указатель текущей позиции выходит за границы буфера данных.

В конце программа CONTROLS закрывает документ HTML, записывая в него теги </BODY> и </HTML>:

printf("</BODY></HTML>");

Если данные передаются в программу CONTROLS методом GET, входные данные находятся в переменной среды QUERY_STRING, которую мы получаем следующим образом:

szQueryString = getenv("QUERY_STRING");

Размер строки определяется при помощи функции strlen:

lSize = strlen(szQueryString);

Обработка принятых данных выполняется аналогично тому, как это делается методом POST. Разница заключается лишь в том, что в документе мы отображаем содержимое только переменных REQUEST_METHOD и QUERY_STRING.

Теперь займемся перекодировкой принятых данных. Она выполняется функцией DecodeStr, исходный текст которой приведен ниже:

void DecodeStr(char *szString)
{
  int src;
  int dst;
  char ch;

  for(src=0, dst=0; szString[src]; src++, dst++)
  {
     ch = szString[src];
     ch = (ch == '+') ? ' ' : ch;
     szString[dst] = ch;
    
     if(ch == '%')
     {
       szString[dst] = DecodeHex(&szString[src + 1]);
       src += 2;
     }
  }
  szString[dst] = '\0';
}

Эта функция сканирует входную строку, заменяя символы «+» на пробелы. Если в перекодируемой строке встречается комбинация символов вида «%xx», мы заменяем ее однобайтовым кодом соответствующего символа с помощью функции DecodeHex.

Функция DecodeHex комбинирует значение кода символа из старшего и младшего разряда преобразуемой комбинации символов:

char DecodeHex(char *str)
{
  char ch;

  if(str[0] >= 'A')
     ch = ((str[0] & 0xdf) - 'A') + 10;
  else
     ch = str[0] - '0';

  ch <<= 4;

  if(str[1] >= 'A')
     ch += ((str[1] & 0xdf) - 'A') + 10;
  else
     ch += str[1] - '0';

  return ch;
}

Программа AREF

В примерах, приведенных выше, мы использовали программы CGI только для обработки данных из полей форм. При этом адрес URL загрузочного файла программы указывался в параметре ACTION тега <FORM>.

Однако есть и другая возможность вызова программ CGI: указать их адрес в параметре HREF тега ссылки <A>. В этом случае Вы можете передать программе CGI параметры, указав их после имени файла загрузочного модуля через разделительный символ «?». Программа CGI получит строку параметров методом GET и сможет извлечь ее из переменной среды с именем QUERY_STRING.

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

Пример документа HTML, в котором демонстрируется вызов программы CGI указанным выше способом, приведен в листинге 7-5.

Листинг 7-5 Вы найдете в файле ch7\aref\aref.html на прилагаемом к книге компакт-диске.

В этом документе есть три ссылки на программу CGI с именем aref.exe, причем каждый раз ей передаются разные параметры:

<a HREF="http://saturn/cgi-bin/aref.exe?page1">
  <p>
Издательство &quot;Русская Редакция&quot;</a><br>
<a HREF="http://saturn/cgi-bin/aref.exe?page2">Microsoft</a><br>
<a HREF="http://saturn/cgi-bin/aref.exe?page3">
Наш сервер</a><br>

Программа CGI принимает параметр и в зависимости от его значения отображает один из документов HTML. Например, при выборе первой строки в окне навигатора отображается главная страница сервера Web издательства «Русская Редакция».

Исходный текст программы AREF приведен в листинге 7-6.

Листинг 7-6 Вы найдете в файле ch7\aref\aref.c на прилагаемом к книге компакт-диске.

Программа получает значение переменной среды QUERY_STRING, пользуясь для этого функцией getenv:

char * szQueryString;
szQueryString = getenv("QUERY_STRING");

Далее она сравнивает значение параметра со строками «page1», «page2» и «page3»:

if(!strcmp(szQueryString, "page1"))
  printf("Location: http://www.rusedit.ru/\n\n");
 
else if(!strcmp(szQueryString, "page2"))
  printf("Location: http://www.microsoft.com/\n\n");
 
else if(!strcmp(szQueryString, "page3"))
  printf("Location: http://www.glasnet.ru/~frolov/\n\n");

else
  printf("Location: error.html\n\n");

При совпадении программа возвращает навигатору адрес URL соответствующего документа HTML, формируя заголовок HTTP специального вида:

Location: «Адрес URL»\n\n

Когда браузер получает от сервера Web такой заголовок, он отображает в своем окне документ или файл графического изображения, адрес URL которого указан в заголовке. В случае ошибки посетителю отправляется документ с именем error.html.

Таким образом, программа CGI анализирует параметры, поступающие от навигатора через ссылку или поля формы, а затем не только динамически формирует документ HTML или ASP для отображения в окне браузера, но и возвращает ссылки на уже существующие документы в виде их адресов URL.

Эта возможность пригодится Вам, например, для организации ссылок на документы через списки, создаваемые тегом <SELECT>, находящимся в форме. Программа CGI определит, какая строка выбрана в списке в момент посылки заполненной формы серверу Web, и в зависимости от этого, либо возвратит ссылку на тот или иной существующий документ, либо сформирует новый документ динамически.

Доступ к базе данных из CGI

Если необходимо, чтобы программа CGI обращалась к базе данных, то для этого можно использовать один из методов доступа, описанный в нашей книге, — ADO, OLE DB или ODBC. Когда Вы извлечете параметры из элементов формы, запустившей программу CGI, передайте их методам или функциям для доступа к базе данных.

В этом разделе мы рассмотрим исходные тексты программы CGI с названием CGICPPADO. Эту программу мы создали на основе консольной программы CPPADO — ее исходные тексты описаны ранее, в четвертой главе книги.

Так как Вы уже познакомились со многими приемами построения программы CGICPPADO, обращаем Ваше внимание только на те моменты, которые имеют отношение к особенностям программы, работающей в качестве приложения CGI.

Для работы с программой CGICPPADO мы подготовили документ HTML с формой, показанной на рис. 7-4.

Рис. 7-4. Форма для ввода идентификатора и пароля

Здесь нужно ввести идентификатор сотрудника магазина, имеющего административные права, а также его пароль. После щелчка кнопки Submit введенная информация передается программе CGICPPADO.

Если идентификатор и пароль указаны правильно и действительно принадлежат сотруднику с административными привилегиями, программа CGICPPADO динамически формирует документ HTML, в котором отображается содержимое таблицы managers базы данных BookStore (рис. 7-5).

Рис. 7-5. Список сотрудников, извлеченный из базы данных программой CGICPPADO

Если же пароль введен неправильно или если пользователь не обладает административными привилегиями, в окне браузера появится лишь сообщение об отказе в доступе (рис. 7-6).

Рис. 7-6. Пользователь не зарегистрирован как администратор, поэтому отображается сообщение об отказе в доступе

Исходные тексты документа HTML с формой приведены в листинге 7-7.

Листинг 7-7 вы найдете в файле ch7\CGICppado\managers.html на прилагаемом к книге компакт-диске.

Здесь Вам следует, прежде всего, обратить внимание на параметры тега <FORM>:

<form method="POST" action="http://saturn/cgi-bin/cppado.exe">

Параметр METHOD задает метод передачи данных из формы как POST, а параметр ACTION определяет путь к загрузочному файлу программы CGICPPADO.

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

<tr>
  <td width="134">Идентификатор:</td> <td width="230">
  <input type="text" name="UserID" size="20"></td>
</tr>
<tr>
  <td width="134">Пароль:</td> <td width="230">
  <input type="password" name="Pwd" size="20"></td>
</tr>

Поле для ввода идентификатора пользователя называется UserID, а поле пароля — Pwd. Эти имена нам потребуются в программе CGICPPADO для извлечения текста из соответствующих элементов формы.

Теперь займемся программой CGICPPADO.

Исходные тексты главного модуля этой программы приведены в листинге 7-8.

Листинг 7-8 Вы найдете в файле ch7\CGICppado\CPPADO.cpp на прилагаемом к книге компакт-диске.

Когда пользователь щелкает кнопку Submit в форме, показанной на рис. 7-4, программа CGICPPADO запускается на сервере Web. При этом управление передается функции _tmain, выполняющей все действия по обращению к базе данных и по динамическому формированию результата этих обращений в виде документов HTML.

В области глобальных переменных мы определили два массива символов, предназначенных для хранения идентификатора и пароля, введенного посетителем в форме:

char szUserID[80];
char szUserPassword[80];

Перед началом работы программа CGICPPADO выводит в стандартный выходной поток заголовок документа HTML:

  cout <<   "Content-type: text/html\n\n";
  cout << "<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 3.2//EN\">";
  cout << "<HTML><HEAD><TITLE>
Просмотр списка сотрудников"
     "</TITLE></HEAD><BODY BGCOLOR=#FFFFFF>";

Здесь мы пользуемся потоком класса cout, что вполне допустимо, так как он связан с потоком stdout.

На следующем этапе наша программа проверяет метод, использованный для передачи данных из формы:

char * szMethod;
szMethod = getenv("REQUEST_METHOD");
if(!strcmp(szMethod, "POST"))
{
  . . .
}
else
  cout << "<h2>Можно использовать только метод POST</h2>";

Если в форме по ошибке применен метод GET, программа запишет сообщение об ошибке в создаваемый документ HTML и завершит свою работу.

В том случае, если использован метод POST, программа определит размер данных, отправленной формой, а затем сохранит их в буфер szBuf:

int lSize;
char szBuf[8196];

lSize = atoi(getenv("CONTENT_LENGTH"));
fread(szBuf, lSize, 1, stdin);
szBuf[lSize] = '\0';

На следующем этапе принятые данные копируются в буфер szSrcBuf и там перекодируются:

char szSrcBuf[8196];
strcpy(szSrcBuf, szBuf);
DecodeStr(szSrcBuf);

Далее программа запускает цикл сканирования принятых данных, чтобы извлечь из них содержимое полей формы (идентификатор пользователя и его пароль):

szBuf[lSize] = '&';
szBuf[lSize + 1] = '\0';

char * szPtr;
char * szParam;
for(szParam = szBuf;;)
{
  szPtr = strchr(szParam, '&');
  if(szPtr != NULL)
  {
     *szPtr = '\0';

     DecodeStr(szParam);
     GetParam(szParam);

     szParam = szPtr + 1;
     if(szParam >= (szBuf + lSize))
       break;
  }
  else
     break;
}

Этот цикл Вам должен быть знаком по программе CONTROLS, описанной ранее в этом разделе. В частности, мы рассказывали о функции DecodeStr. Здесь, однако, мы дополнительно вызываем функцию GetParam, извлекающую идентификатор пользователя и пароль и сохраняющую эти данные в глобальных переменных szUserID и szUserPassword.

Исходный текст функции GetParam показан ниже:

void GetParam(char *szString)
{
  char szBuf[1024];
  char * szPtr;

  strcpy(szBuf, szString);
  szPtr = strchr(szBuf, '=');
  if(szPtr != NULL)
  {
     *szPtr = '\0';
     szPtr++;

     if(!strncmp(szBuf, "UserID", 6))
       strcpy((char*)szUserID, szPtr);

     else if(!strncmp(szBuf, "Pwd", 3))
       strcpy((char*)szUserPassword, szPtr);
  }
}

Получая через параметр szString указатель на пару вида «Имя_параметра=Значение», эта функция последовательно сравнивает имя со строками «UserID» и «Pwd». При совпадении извлеченное значение содержимого соответствующего поля формы сохраняется в глобальных переменных szUserID и szUserPassword.

После обработки данных, поступивших от формы, программа CGICPPADO проверяет идентификатор и пароль пользователя, вызывая функцию login:

if(login((char*)szUserID, (char*)szU serPassword))
  getManagers();
else
  cout << "<h2>
Доступ запрещен</h2>";

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

Функция login обращается к базе данных для проверки идентификатора, пароля, а также прав пользователя. Если запрос сделал пользователь с правами администратора, функция login возвращает значение true, а если нет — false.

После успешной проверки прав пользователя вызывается функция getManagers. Она записывает в динамически формируемый документ HTML сведения о содержимом таблицы managers.

Перед завершением своей работы программа CGICPPADO записывает в стандартный выходной поток теги, завершающие формирование документа HTML:

cout << "</BODY></HTML>";

Функция login практически совпадает с одноименной функцией из приложения CPPADO, описанной нами в четвертой главе. Однако есть и отличия — новый вариант функции принимает через свои параметры имя и пароль пользователя, а не вводит их из стандартного потока:

bool login(char* szUsername, char * szPassword)
{
  . . .
}

Функцию getManagers мы также взяли из приложения CPPADO, изменив только формат вывода информации, извлеченной из базы данных.

Перед началом цикла обработки набора записей эта функция формирует заголовок таблицы в документе HTML, записывая в него названия столбцов:

cout << "<h2>Список сотрудников магазина<h2>";
cout << "<table border=1>";
cout << "<th>Идентификатор</th><th>Имя</th>"
  "<th>Пароль</th><th>Последнее подключение</th><th>Права</th>";

Значения, извлеченные из полей текущей записи набора, форматируются для записи в ячейки таблицы, создаваемой в документе HTML:

while(rs->adoEOF == VARIANT_FALSE)
{
  vManagerID = rs->Fields->GetItem(_variant_t("ManagerID"))->Value;
  vName       = rs->Fields->GetItem(_variant_t("Name"))->Value;
  vPassword = rs->Fields->GetItem(_variant_t("Password"))->Value;
  vLastLogin = rs->Fields->GetItem(_variant_t("LastLogin"))->Value;
  vRights     = rs->Fields->GetItem(_variant_t("Rights"))->Value;

  strTmp.Format(
  "<td>%s</td><td>%10s</td><td>%10s</td><td>%20s</td><td>%10s</td>",
  v2str(vManagerID), v2str(vName),
  v2str(vPassword), v2str(vLastLogin),
  v2str(vRights));

  cout << "<tr>";
  cout << (LPCTSTR)strTmp << "\n";
  cout << "</tr>";

  hr = rs->MoveNext();
  if(!SUCCEEDED(hr))
     break;
}

При этом для каждой строки набора записей мы формируем одну строку таблицы в выходном документе HTML.

Перед завершением своей работы функция getManagers записывает в выходной поток закрывающий тег таблицы:

cout << "</table>";

Обработка ошибок, возникающих при обращении к базе данных, выполняется при помощи функции AdoErrHandler:

catch(_com_error ex)
{
  AdoErrHandler(cn);
  return;
}

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

Создание приложений ISAPI

В этом разделе речь пойдет о приложениях ISAPI, дополняющих возможности сервера Microsoft Information Server. Все эти приложения можно разделить на две группы: расширения ISAPI и фильтры ISAPI.

Первые по своему назначению напоминают только что изученные Вами программы CGI. Однако в отличие от последних эти расширения выполнены в виде библиотек динамической загрузки DLL, что имеет ряд преимуществ.

Так же как и программы CGI, расширения ISAPI получают данные от браузера (например, из формы, заполненной посетителем сервера Web), обрабатывают их и посылают ответ браузеру в виде динамически сформированного документа HTML. Однако вместо чтения содержимого переменных среды и стандартного потока ввода расширение ISAPI получает данные при помощи специально предназначенных для этого функций. Аналогично вместо записи выходных данных в стандартный поток вывода расширение ISAPI вызывает специальные функции.

Фильтры ISAPI также реализуются в виде библиотек DLL, однако они имеют другое назначение. Фильтры ISAPI способны контролировать весь поток данных, проходящий через сервер, на уровне протокола HTTP. Поэтому их можно применять для решения таких задач, как шифрование или перекодирование данных, компрессия информации. Они пригодны для создания собственных процедур подключения пользователей к системе и аутентификации (проверки идентификации пользователей), а также для сбора статистической информации использования ресурсов сервера.

Принципы работы и структура расширения ISAPI

Как только что было сказано, расширение ISAPI создается в виде библиотеки динамической загрузки DLL. Обращение к такой библиотеке выполняется в документах HTML аналогично обращению к программам CGI — из форм или ссылок, созданных при помощи тегов <FORM> и <A>.

Когда пользователь обращается к расширению ISAPI, соответствующая библиотека DLL загружается в адресное пространство сервера Microsoft Information Server и становится его составной частью. Так как расширение ISAPI работает в рамках процесса сервера Microsoft Information Server, а не в рамках отдельного процесса (как это происходит при запуске программы CGI), оно может пользоваться всеми ресурсами, доступными серверу. Это благоприятно сказывается на производительности.

Производительность сохраняется на достаточно высоком уровне и в тех случаях, когда расширение сервера используется активно сразу многими пользователями.

Если, скажем, 20 пользователей запустят одновременно одну и ту же программу CGI, то на сервере будет создано 20 процессов — по одному для каждого пользователя. Так как создание процесса отнимает достаточно много системных ресурсов, это приведет к потере производительности.

Если же 20 пользователей одновременно обратятся к одному и тому же расширению ISAPI, в память серверного процесса будет загружена одна копия библиотеки DLL, содержащая расширение. Она будет работать в многопоточном режиме. Очевидно, при этом полностью исключаются накладные расходы системных ресурсов на запуск процессов.

Сравнивая программы CGI и расширения ISAPI, нужно заметить, что, несмотря на существенное превосходство в быстродействии расширений ISAPI, программы CGI также имеют свои преимущества.

Так как расширения ISAPI работают в рамках серверного процесса, они должны отлаживаться особенно тщательно. Ошибка в расширении ISAPI способна вызвать аварийное завершение работы всего сервера Microsoft Information Server. Что же касается программы CGI, выполняющейся как отдельный процесс в своем собственном адресном пространстве, то она едва ли способна вывести из строя сервер. Если в программе CGI допущена критическая ошибка, это приведет всего лишь к аварийному завершению самой программы, но не сервера.

Напомним, что расширение ISAPI работает в многопоточном режиме, что приводит к дополнительным проблемам при отладке.

Вызов расширения ISAPI сервером Web

Структура расширения ISAPI очень проста. Библиотека DLL расширения должна экспортировать всего две функции с именами GetExtensionVersion и HttpExtensionProc. Первая предназначена для того, чтобы расширение могло сообщить серверу версию спецификации, которой оно соответствует, и строку описания расширения. Функция HttpExtensionProc выполняет всю работу по передаче данных между расширением и сервером.

Дополнительно расширение ISAPI способно экспортировать функцию TerminateExtension. Она вызывается сервером перед тем, как ненужное больше приложение ISAPI выгружается из памяти. Функция TerminateExtension должна освободить ресурсы, полученные при инициализации расширения ISAPI.

Функция GetExtensionVersion

Функция GetExtensionVersion очень проста в реализации и обычно выглядит следующим образом:

BOOL WINAPI GetExtensionVersion(HSE_VERSION_INFO *pVersion)
{
  pVersion->dwExtensionVersion =
     MAKELONG(HSE_VERSION_MINOR,HSE_VERSION_MAJOR);

  lstrcpyn(pVersion->lpszExtensionDesc,
     "My ISAPI Application Name", HSE_MAX_EXT_DLL_NAME_LEN);

  return TRUE;
}

При вызове функции GetExtensionVersion передается через единственный параметр указатель на структуру типа HSE_VERSION_INFO. Эта структура и указатель на нее (типа LPHSE_VERSION_INFO) определены в файле httpext.h следующим образом:

#define   HSE_MAX_EXT_DLL_NAME_LEN 256
typedef struct _HSE_VERSION_INFO
{
  DWORD dwExtensionVersion;
  CHAR lpszExtensionDesc[HSE_MAX_EXT_DLL_NAME_LEN];
} HSE_VERSION_INFO, *LPHSE_VERSION_INFO;

Константы HSE_VERSION_MINOR и HSE_VERSION_MAJOR указывают текущую версию интерфейса расширения ISAPI и также определены в файле httpext.h:

#define HSE_VERSION_MAJOR 4 // верхний номер версии
#define HSE_VERSION_MINOR 0 // нижний номер версии

Функция HttpExtensionProc

Теперь рассмотрим вторую функцию, которую должна экспортировать библиотека DLL расширения ISAPI. Она называется HttpExtensionProc и имеет следующий прототип:

DWORD WINAPI HttpExtensionProc(EXTENSION_CONTROL_BLOCK *pECB);

Функция HttpExtensionProc получает единственный параметр — указатель на структуру типа EXTENSION_CONTROL_BLOCK, определенную в файле httpext.h:

typedef struct _EXTENSION_CONTROL_BLOCK 
{
  DWORD cbSize;    // размер структуры в байтах
  DWORD dwVersion; // версия спецификации ISAPI
  HCONN ConnID;    // идентификатор канала
  DWORD dwHttpStatusCode; // код состояния HTTP

  CHAR lpszLogData[HSE_LOG_BUFFER_LEN]; // текстовая строка,
          // закрытая двоичным нулем, в которой находится информация
          // протоколирования, специфичная для данного расширения

  LPSTR lpszMethod;         // переменная REQUEST_METHOD
  LPSTR lpszQueryString;    // переменная QUERY_STRING
  LPSTR lpszPathInfo;       // переменная PATH_INFO
  LPSTR lpszPathTranslated; // переменная PATH_TRANSLATED

  DWORD  cbTotalBytes;    // полный размер данных, полученных от
                          // браузера
  DWORD  cbAvailable;     // размер доступного блока данных
  LPBYTE lpbData;         // указатель на доступный блок данных
                          // размером cbAvailable байт
  LPSTR  lpszContentType; // тип принятых данных

  // Функция GetServerVariable для получения значения переменных
  BOOL (WINAPI * GetServerVariable)(HCONN hConn,
      LPSTR lpszVariableName, LPVOID lpvBuffer, LPDWORD lpdwSize);

  // Функция WriteClient для посылки данных посетителю
  BOOL (WINAPI * WriteClient)(HCONN ConnID,
      LPVOID Buffer, LPDWORD lpdwBytes, DWORD dwReserved);

  // Функция ReadClient для получения данных от посетителя
  BOOL (WINAPI * ReadClient) (HCONN ConnID,
      LPVOID lpvBuffer, LPDWORD lpdwSize);

  // Вспомогательная функция ServerSupportFunction
  // для выполнения различных операций
  BOOL (WINAPI * ServerSupportFunction)(HCONN hConn,
      DWORD dwHSERRequest, LPVOID lpvBuffer,
      LPDWORD lpdwSize, LPDWORD lpdwDataType);

} EXTENSION_CONTROL_BLOCK, *LPEXTENSION_CONTROL_BLOCK;

Рассмотрим отдельные поля этой структуры, представляющей большой интерес для разработчиков расширений ISAPI.

·    cbSize

В самом начале структуры EXTENSION_CONTROL_BLOCK находится поле cbSize, в которое при вызове расширения сервер записывает размер структуры в байтах.

·    dwVersion

Поле dwVersion содержит номер версии расширения ISAPI. Верхнее и нижнее значения номера версии можно получить при помощи макрокоманд HIWORD и LOWORD, соответственно.

·    ConnID

В поле ConnID сервер записывает идентификатор канала, созданного для расширения. Это поле нельзя изменять.

·    dwHttpStatusCode

Поле dwHttpStatusCode должно заполняться расширением ISAPI. Вы должны записать сюда результат завершения операции (код состояния транзакции). В случае успеха в это поле записывается значение 200 (как указано в спецификации HTTP).

·    lpszLogData

Поле lpszLogData предназначено для записи сообщения о выполнении транзакции в журнал сервера Web. Это сообщение должно быть в виде текстовой строки, закрытой нулем. Размер строки в байтах не должен превышать значения HSE_LOG_BUFFER_LEN.

·    lpszMethod

Поле lpszMethod заполняется сервером и содержит название метода передачи данных от удаленного пользователя серверу в виде текстовой строки, закрытой двоичным нулем. Расширения ISAPI используют те же самые методы, что и программы CGI — метод GET и метод POST. Проводя аналогию с программами CGI дальше, скажем, что поле lpszMethod эквивалентно переменной среды с именем REQUEST_METHOD, создаваемой для программы CGI.

·    lpszQueryString

Аналогично, поле lpszQueryString соответствует переменной среды с именем QUERY_STRING. В это поле записываются данные, принятые от удаленного пользователя методом GET.

·    lpszPathInfo

В поле lpszPathInfo записывается виртуальный путь к программному файлу библиотеки DLL расширения ISAPI. Напомним, что аналогичная информация для программ CGI передавалась через переменную среды с именем PATH_INFO.

·    lpszPathTranslated

Это поле содержит физический путь к программному файлу библиотеки DLL расширения ISAPI. Оно соответствует переменной среды с именем PATH_TRANSLATED, создаваемой для программ CGI.

·    cbTotalBytes

В поле cbTotalBytes записывается общее количество байт данных, которое необходимо получить от удаленного пользователя. Часть этих данных (размером не более 48 кб) считывается сервером автоматически. Эти данные будут сразу доступны, после того как функция HttpExtensionProc получит управление. Остальные данные необходимо дочитать в цикле при помощи функции ReadClient, о которой мы еще будем говорить.

·    cbAvailable

В поле cbAvailable записывается размер блока данных, полученных автоматически от браузера посетителя сервера. Как мы только что сказали, размер этого блока не может превышать 48 кб. Этого, однако, вполне достаточно для обработки данных, полученных от форм обычного размера.

·    lpbData

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

·    lpszContentType

Поле lpszContentType содержит тип принятых данных, например, «text/html».

·    GetServerVariable

Помимо полей данных, структура EXTENSION_CONTROL_BLOCK содержит указатели на функции. С помощью этих функций расширение ISAPI может выполнять различные операции, такие как прием данных от удаленного пользователя.

Поле GetServerVariable содержит указатель на функцию, средствами которой расширение ISAPI может получить информацию, доступную программам CGI через переменные среды.

·    WriteClient

В поле WriteClient находится адрес функции, которую расширение ISAPI должно использовать для отправки данных удаленному пользователю. Таким образом, вместо того чтобы записывать данные в стандартный поток вывода, как это делает программа CGI, приложение ISAPI посылает данные с помощью функции WriteClient.

·    ReadClient

Посредством функции, адрес которой передается в поле ReadClient, приложение может дочитать дополнительные данные, не поместившиеся в буфер предварительного чтения, имеющий адрес lpbData и размер, не превышающий 48 кб. Аналогичную операцию приема данных от пользователя выполняет программа CGI в случае применения метода передачи данных POST. Отличие заключается в том, что программа CGI получает данные через стандартный поток ввода, а расширение ISAPI берет эти данные из буфера предварительного чтения и при необходимости дочитывает данные при помощи функции ReadClient.

·    ServerSupportFunction

Посредством функции, адрес которой передается через поле ServerSupportFunction, расширение ISAPI может выполнять различные действия, такие как посылка стандартного заголовка протокола HTTP и некоторые другие.

При успешном завершении функция HttpExtensionProc должна вернуть значение HSE_STATUS_SUCCESS, а при ошибке — значение HSE_STATUS_ERROR. Соответствующие константы определены в файле httpext.h.

Получение данных расширением ISAPI

Программа CGI получает данные из переменных среды и стандартного потока ввода (в случае применения метода доступа POST). Расширение ISAPI делает это по-другому.

Функция HttpExtensionProc получает указатель на структуру типа EXTENSION_CONTROL_BLOCK. Некоторые поля этой структуры заполняются сервером и должны использоваться для получения входных данных. Прежде всего это поле lpszMethod, через которое передается метод, использованный для посылки данных (GET или POST), поле lpszQueryString, в котором передаются параметры запуска расширения или данные при использовании метода GET, а также другие поля, описанные выше.

Через структуру EXTENSION_CONTROL_BLOCK передаются адреса функций GetServerVariable и ReadClient, специально предназначенных для получения данных от браузера посетителя.

Функция GetServerVariable

Прототип функции GetServerVariable определен в структуре EXTENSION_CONTROL_BLOCK, описанной нами ранее:

BOOL (WINAPI * GetServerVariable)(HCONN hConn,
  LPSTR lpszVariableName, LPVOID lpvBuffer, LPDWORD lpdwSize);

Через параметр hConn Вы должны передать этой функции идентификатор канала, полученный через поле ConnID структуры EXTENSION_CONTROL_BLOCK.

Параметр lpszVariableName должен содержать указатель на строку имени переменной, содержимое которой необходимо получить. Это содержимое будет записано функцией в буфер, адрес которого передается через параметр lpvBuffer, а размер — через параметр lpdwSize.

Ниже перечислены возможные значения строк, передаваемых через параметр lpszVariableName.

·    AUTH_TYPE

Переменная среды AUTH_TYPE содержит тип идентификации, который применяется сервером.

·    HTTP_ACCEPT

В этой переменной перечислены типы данных MIME, которые могут быть приняты браузером от сервера Web.

·    CONTENT_LENGTH

Количество байт данных, которые расширение ISAPI должно получить от браузера.

·    CONTENT_TYPE

Тип данных, присланных браузером.

·    PATH_INFO

Путь к виртуальному каталогу, в котором находится библиотека DLL расширения ISAPI.

·    PATH_TRANSLATED

Физический путь к библиотеки DLL расширения ISAPI.

·    QUERY_STRING

Строка параметров, указанная в форме или теге ссылки <A>. Эта строка указывается после адреса URL библиотеки DLL расширения ISAPI вслед за разделительным символом «?».

·    REMOTE_ADDR

Адрес IP узла, на котором работает браузер посетителя.

·    REMOTE_HOST

Доменное имя узла, на котором работает браузер посетителя. Если эта информация недоступна (например, для узла не определен доменный адрес), то вместо доменного имени указывается адрес IP, как в переменной REMOTE_ADDR.

·    REMOTE_USER

Имя пользователя, которое применяет браузер для аутентификации.

·    UNMAPPED_REMOTE_USER

Имя пользователя до обработки фильтром ISAPI, которое применяет браузер для аутентификации.

·    REQUEST_METHOD

Метод доступа, который используется для передачи данных от браузера серверу Web.

·    SCRIPT_NAME

В эту переменную записывается путь к виртуальному каталогу и имя библиотеки DLL расширения ISAPI. Анализируя эту переменную, расширение ISAPI определит путь к своему загрузочному файлу.

·    SERVER_NAME

Доменное имя сервера Web или адрес IP сервера Web, если доменное имя недоступно или не определено.

·    SERVER_PROTOCOL

Имя и версия протокола, который применяется для выполнения запроса к расширению ISAPI.

·    SERVER_PORT

Номер порта, на котором браузер посылает запросы серверу Web.

·    SERVER_PORT_SECURE

Если обработка запроса выполняется через защищенный порт, в этой строке записано значение 1, а если через незащищенный — значение 0.

·    SERVER_SOFTWARE

Название и версия программного обеспечения сервера Web. Версия следует после названия и отделяется символом «/».

·    ALL_HTTP

Строка, закрытая двоичным нулем, в которую записаны значения всех переменных, имеющих отношение к протоколу HTTP. Это, например, такие переменные как HTTP_ACCEPT, HTTP_CONNECTION, HTTP_USER_AGENT и т. д.

Извлекать содержимое отдельных переменных Ваша программа должна самостоятельно. При этом следует учесть, что названия переменных отделены от их значений символом двоеточия «:», а поля переменных разделены символом перевода строки.

Обратите внимание, что названия этих строк почти совпадают с названиями переменных среды, создаваемых для программ CGI, однако совпадение все же не полное.

В случае успешного завершения функция GetServerVariable возвращает значение TRUE, а при возникновении ошибки — значение FALSE. Код ошибки можно определить с помощью функции GetLastError, вызвав ее сразу после функции GetServerVariable.

Возможные коды ошибок для функции GetServerVariable приведены в таблице 7-2.

Таблица 7-2. Коды ошибок для функции GetServerVariable

Код ошибки

Описание

ERROR_INVALID_INDEX

Неправильное имя переменной, передаваемой через параметр lpszVariableName

ERROR_INVALID_PARAMETER

Неправильное значение параметра Hconn

ERROR_INSUFFICIENT_BUFFER

Буфер, адрес которого указан с помощью параметра lpvBuffer, слишком мал. Необходимый размер буфера записывается по адресу, который был передан функции через параметр lpdwSize

ERROR_MORE_DATA

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

ERROR_NO_DATA

Данные не получены

Ниже мы показываем пример использования функции GetServerVariable для получения содержимого переменной с именем ALL_HTTP в буфер szTempBuf.

CHAR szTempBuf[4096];
DWORD dwSize;
dwSize = 4096;
lpECB->GetServerVariable(lpECB->ConnID,
  (LPSTR)"ALL_HTTP", (LPVOID)szTempBuf, &dwSize);
strcat(szBuff, szTempBuf);

Функция ReadClient

Прототип функции ReadClient записан в определении структуры EXTENSION_CONTROL_BLOCK и выглядит следующим образом:

BOOL (WINAPI * ReadClient) (HCONN ConnID,
  LPVOID lpvBuffer, LPDWORD lpdwSize);

Через параметр hConn этой функции надо передать идентификатор канала, полученный через поле ConnID структуры EXTENSION_CONTROL_BLOCK.

Функция ReadClient читает данные в буфер, адрес которого передается через параметр lpvBuffer, а размер — через параметр lpdwSize. В случае успеха функция возвращает значение TRUE, а при ошибке — значение FALSE. Код ошибки можно получить посредством функции GetLastError.

Работа с функцией ReadClient имеет некоторые особенности.

Когда расширение ISAPI получает управление, через структуру типа EXTENSION_CONTROL_BLOCK передается адрес предварительно прочитанного блока данных, полученного от браузера посетителя. Как Вы знаете, адрес и размер этого блока данных указаны соответственно в полях lpbData и cbAvailable структуры EXTENSION_CONTROL_BLOCK.

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

Прежде всего следует сравнить размер предварительно считанных данных с полным размером данных, которые нужно считать (этот размер передается в поле cbTotalBytes структуры EXTENSION_CONTROL_BLOCK).

Если все данные уже считаны, функцию ReadClient вызывать не нужно. В том случае, когда значение, передаваемое через поле cbTotalBytes, превышает значение cbAvailable, Вы должны воспользоваться функцией ReadClient для того чтобы прочесть cbTotalBytes-cbAvailable байт данных от пользователя.

Заметим, что функция ReadClient не будет читать заново данные, записанные в буфер lpbData. Она займется оставшимися данными, причем не исключено, что для их прочтения эту функцию придется вызывать в цикле несколько раз. Дело в том, что функция ReadClient не всегда может прочитать все оставшиеся данные за один прием.

После успешного завершения чтения функция ReadClient записывает размер прочитанного блока данных в переменную, адрес которой передается через параметр lpdwSize. Если при первом вызове этот размер меньше величины cbTotalBytes-cbAvailable, Вы должны вызвать функцию ReadClient еще один или несколько раз для чтения оставшихся данных.

Отправка данных расширением ISAPI

Вместо того чтобы записывать выходные данные в стандартный поток вывода, как это делает программа CGI, расширение ISAPI пользуется для пересылки данных функциями WriteCilent и ServerSupportFunction. Указатели на эти функции передаются расширению ISAPI через структуру типа EXTENSION_CONTROL_BLOCK.

Функция WriteCilent

Прототип функции WriteClient, взятый из определения структуры EXTENSION_CONTROL_BLOCK, приведен ниже:

BOOL (WINAPI * WriteClient)(HCONN ConnID,
  LPVOID Buffer, LPDWORD lpdwBytes, DWORD dwReserved);

Через параметр hConn функции WriteClient передается идентификатор канала, полученный через поле ConnID структуры EXTENSION_CONTROL_BLOCK.

Функция WriteClient посылает удаленному пользователю данные из буфера Buffer, причем размер передаваемого блока данных должен быть записан в переменную типа DWORD, адрес которой передается через параметр lpdwBytes. Параметр dwReserved зарезервирован для дальнейших расширений возможностей функции.

В случае успеха функция возвращает значение TRUE, а при ошибке — значение FALSE. Код ошибки можно получить посредством функции GetLastError.

Заметим, что после отпрвки данных функция WriteClient записывает в переменную, адрес которой был ей передан через параметр lpdwBytes, количество успешно переданных байт данных. В отличие от функции ReadClient, функция WriteClient посылает данные за один прием, поэтому нет необходимости вызывать ее в цикле. Если же эта функция смогла передать только часть данных, то это означает, что произошла ошибка.

Функция ServerSupportFunction

Прототип функции ServerSupportFunction, определенный в структуре типа EXTENSION_CONTROL_BLOCK, приведен ниже:

BOOL (WINAPI * ServerSupportFunction)(HCONN hConn,
 DWORD dwHSERRequest, LPVOID lpvBuffer,
 LPDWORD lpdwSize, LPDWORD lpdwDataType);

Через параметр hConn функции ServerSupportFunction передается идентификатор канала, полученный через поле ConnID структуры EXTENSION_CONTROL_BLOCK.

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

Через параметр lpvBuffer передается размер буфера, который используется при выполнении операции. Размер этого буфера должен быть записан в переменной типа DWORD, адрес которой передается через параметр lpdwSize. После выполнения операции передачи данных в эту переменную будет записан размер успешно переданного блока данных.

Параметр lpdwDataType применяется для указания дополнительной строки заголовка или дополнительных данных, которые будут добавлены к заголовку, передаваемому удаленному пользователю. Если для параметра lpdwDataType указать значение NULL (что допустимо), к заголовку будут добавлены символы конца строки «\r\n».

Какие операции допустимо выполнять при помощи функции ServerSupportFunction?

Ниже мы привели список возможных значений параметра dwHSERRequest, определяющего код выполняемой операции.

·   HSE_REQ_SEND_RESPONSE_HEADER

Эта операция предназначена для пересылки удаленному пользователю стандартного заголовка HTTP. Если надо добавить другие заголовки, следует воспользоваться параметром lpdwDataType. В качестве дополнительного заголовка Вы можете указать любую строку, закрытую символами конца строки «\r\n» и двоичным нулем.

Если Ваше расширение ISAPI динамически формирует документ HTML и отправляет его пользователю, то ей не нужно вызывать функцию WriteClient.

Все необходимые для этого действия можно сделать средствами одной только функции ServerSupportFunction. Ниже мы показали фрагмент кода, в котором эта функция используется для посылки документа HTML, подготовленного заранее в буфере szBuff:

CHAR szBuff[4096];
wsprintf(szBuff,
  "Content-Type: text/html\r\n\r\n"
  "<HTML><HEAD><TITLE>Simple ISAPI Extension</TITLE></HEAD>\n"
  "<BODY BGCOLOR=#FFFFFF><H1>Hello from ISAPI Extension!</H1>\n");

strcat(szBuff, "<H1>Заголовок документа</H1>");
strcat(szBuff, "<HR>");
strcat(szBuff, "</BODY></HTML>");  

lpECB->ServerSupportFunction(lpECB->ConnID,
  HSE_REQ_SEND_RESPONSE_HEADER, NULL, NULL, (LPDWORD)szBuff);

Заметим, однако, что функция ServerSupportFunction не позволяет посылать двоичные данные. Для отправки двоичных данных Вы обязательно должны применить функцию WriteClient.

·   HSE_REQ_SEND_URL

Используя операцию HSE_REQ_SEND_URL, расширение ISAPI может послать удаленному пользователю данные, хранящиеся по определенному адресу URL, как будто бы эти данные были запрошены непосредственно пользователем по этому адресу URL. Это удобно для отправки либо динамически созданных данных, либо предварительно подготовленных данных.

Адрес URL должен быть указан в виде текстовой строки, закрытой двоичным нулем, через параметр lpvBuffer. Размер строки задают в параметре lpdwSize. Что же касается параметра lpdwDataType, то при выполнении данной операции этот параметр игнорируется.

·   HSE_REQ_SEND_URL_REDIRECT_RESP

Отправка сообщения с номером 302 (URL Redirect). Адрес URL указывается аналогично тому, как это делается при выполнении операции HSE_REQ_SEND_URL.

·   HSE_REQ_MAP_URL_TO_PATH

Преобразование логического адреса URL в физический. Адрес логического пути передается через параметр lpvBuffer. По этому же адресу записывается результат преобразования. Размер буфера при вызове функции задается как обычно с помощью параметра lpdwSize.

После выполнения преобразования в переменную типа DWORD, адрес которой указан параметром lpdwSize, будет записана длина строки результата преобразования.

·   HSE_REQ_DONE_WITH_SESSION

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

Приложение ISHELLO

В качестве нашего первого расширения ISAPI мы предлагаем приложение ISHELLO, выполняющее простейшие функции.

Вызов расширения ishello.dll выполняется из формы, исходный текст которой приведен в листинге 7-9.

Листинг 7-9 Вы найдете в файле ch7\ISHELLO\ishello.html на прилагаемом к книге компакт-диске.

Расширение вызывается в параметре ACTION тега <FORM> аналогично тому, как это делается для программ CGI:

<form METHOD="POST"
ACTION="http://saturn/cgi-bin/ishello.dll?Param1|Param2|Param3">
<p><input TYPE="submit" VALUE="Send"> </p>
</form>

После вызова наше расширение ishello.dll динамически создает документ HTML, представленный на рис. 7-7.

Рис. 7-7. Документ HTML, созданный динамически расширением ishello.dll

В верхней части этого документа отображается содержимое некоторых полей структуры EXTENSION_CONTROL_BLOCK, а в нижней отображается содержимое переменной ALL_HTTP, полученное с помощью функции GetServerVariable.

Исходный текст расширения ishello.dll представлен в листинге 7-10.

Листинг 7-10 Вы найдете в файле ch7\ISHELLO\ishello.с на прилагаемом к книге компакт-диске.

Наряду с обычным для приложений Windows файлом windows.h мы включили в наш исходный текст файл httpext.h, в котором определены все необходимые константы, структуры данных и прототипы функций:

#include <windows.h>
#include <httpext.h>

Этот файл поставляется в составе Microsoft Visual C++.

В приложении определена функция GetExtensionVersion — ее мы уже рассматривали ранее. Она записывает версию интерфейса ISAPI и текстовую строку описания расширения в поля структуры типа HSE_VERSION_INFO с именами dwExtensionVersion и lpszExtensionDesc, соответственно. Адрес структуры HSE_VERSION_INFO передается функции GetExtensionVersion через параметр.

Функция HttpExtensionProc обращается к буферу szBuff для подготовки динамически создаваемого документа HTML, который будет послан удаленному пользователю в результате работы нашего расширения. В качестве вспомогательного буфера применяется буфер szTempBuf:

CHAR szBuff[4096];
CHAR szTempBuf[4096];

Прежде всего мы записываем в поле dwHttpStatusCode нулевое значение:

lpECB->dwHttpStatusCode = 0;

Потом в это поле мы запишем результат выполнения команды.

Далее в буфер szBuff копируется заголовок HTTP и начальный фрагмент документа HTML, для чего используется функция wsprintf:

wsprintf(szBuff,
  "Content-Type: text/html\r\n\r\n"
  "<HTML><HEAD><TITLE>Simple ISAPI Extension</TITLE></HEAD>\n"
  "<BODY BGCOLOR=#FFFFFF><H2>Hello from ISAPI Extension!</H2>\n");

После этого к буферу szBuff с помощью функции strcat добавляются другие строки документа. Например, разделительная линия:

strcat(szBuff, "<HR>");

После первой разделительной линии в документ вставляются несколько строк со значениями некоторых полей структуры типа EXTENSION_CONTROL_BLOCK. В следующем фрагменте кода показана строка версии интерфейса ISAPI:

wsprintf(szTempBuf, "<P>Extension Version: %d.%d",
  HIWORD(lpECB->dwVersion), LOWORD(lpECB->dwVersion));
strcat(szBuff, szTempBuf);

Далее в документ выводятся строка с названием метода передачи данных (поле lpszMethod), строка параметров запуска расширения ISAPI (поле lpszQueryString), физический путь к программному файлу библиотеки DLL расширения (поле lpszPathTranslated), полный размер данных, которые нужно прочитать (поле cbTotalBytes), а также тип данных (поле lpszContentType):

wsprintf(szTempBuf, "<BR>Method: %s", lpECB->lpszMethod);
strcat(szBuff, szTempBuf);
 
wsprintf(szTempBuf, "<BR>QueryString: %s",
  lpECB->lpszQueryString);
strcat(szBuff, szTempBuf);
 
wsprintf(szTempBuf, "<BR>PathTranslated: %s",
  lpECB->lpszPathTranslated);
strcat(szBuff, szTempBuf);

wsprintf(szTempBuf, "<BR>TotalBytes: %d",
  lpECB->cbTotalBytes);
strcat(szBuff, szTempBuf);

wsprintf(szTempBuf, "<BR>ContentType: %s",
  lpECB->lpszContentType);
strcat(szBuff, szTempBuf);

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

strcat(szBuff, "<HR><P><B>Server Variables:</B><BR>");

dwSize = 4096;
lpECB->GetServerVariable(lpECB->ConnID,
  (LPSTR)"ALL_HTTP", (LPVOID)szTempBuf, &dwSize);
strcat(szBuff, szTempBuf);

В завершение в документ записывается финальная строка:

strcat(szBuff, "</BODY></HTML>");

Сформированный таким образом документ отправляется посетителю сервера Web функцией ServerSupportFunction, как это показано ниже:

if(!lpECB->ServerSupportFunction(lpECB->ConnID,
  HSE_REQ_SEND_RESPONSE_HEADER, NULL, NULL, (LPDWORD)szBuff))
{
  return HSE_STATUS_ERROR;
}
lpECB->dwHttpStatusCode = 200;
return HSE_STATUS_SUCCESS;

Если при пересылке данных произошла ошибка, расширение завершает свою работу с кодом HSE_STATUS_ERROR. В случае успеха в поле состояния dwHttpStatusCode записывается код 200. Вслед за этим расширение завершает свою работу с кодом HSE_STATUS_SUCCESS.

Создавая проект расширения ISAPI, Вы должны подготовить файл определения модуля для соответствующей библиотеки DLL (листинг 7-11).

Листинг 7-11 хранится в файле ch7\ISHELLO\ishello.def на прилагаемом к книге компакт-диске.

В разделе EXORT этого файла нужно указать имена функций GetExtensionVersion и HttpExtensionProc:

LIBRARY       ishello
DESCRIPTION 'Simple ISAPI DLL'
EXPORTS
  GetExtensionVersion

  HttpExtensionProc

Вызов функций ODBC из ISAPI

Ранее мы рассказывали, как обращаться к базе данных из программы CGI с применением объектного интерфейса ADO. В этом разделе мы приведем исходные тексты расширения ISAPI, которое тоже работает с базой данных, но с применением программного интерфейса ODBC.

Для запуска этого приложения ISAPI (с названием ISFORM) мы используем форму, показанную на рис. 7-8.

Рис. 7-8. Форма ввода идентификатора и пароля

С помощью этой формы сотрудник нашего магазина сможет просмотреть свои собственные права. Для этого ему придется ввести в ней свой идентификатор и пароль, а затем щелкнуть кнопку Submit.

Расширение ISFORM обращается к базе данных BookStore, запуская на выполнение хранимую процедуру ManagerLogin. Если пользователь зарегистрирован в таблице managers, расширение ISFORM отправляет ему динамически созданный документ HTML, в котором отображается идентификатор пользователя, его пароль и права, извлеченные из этой таблицы (рис. 7-9).

Рис. 7-9. Перечень прав сотрудника магазина, извлеченные из базы данных

Похожие действия выполняла консольная программа ODBCPARAM, рассмотренная нами в предыдущей главе. Именно эта программа и была положена в основу при создании расширения ISFORM.

А теперь опишем исходные тексты приложения ISFORM.

Исходный текст документа HTML, предназначенного для запуска этого расширения, Вы найдете в листинге 7-12.

Листинг 7-12 хранится в файле ch7\ISFORM\managers.html на прилагаемом к книге компакт-диске.

В этом документе имеется форма, ссылающаяся на загрузочный файл расширения isform.dll:

<form method="POST" action="http://saturn/cgi-bin/isform.dll">

Помимо всего прочего, в этой форме определены два поля, предназначенные для ввода идентификатора и пароля:

<tr>
  <td width="134">Идентификатор:</td><td width="230">
  <input type="text" name="UserID" size="20"></td>
</tr>
<tr>
  <td width="134">Пароль:</td><td width="230">
  <input type="password" name="Pwd" size="20"></td>
</tr>

Данные из этих полей передаются расширению ISFORM.

Исходный текст главного модуля приложения ISFORM приведен в листинге 7-13.

Листинг 7-13 хранится в файле ch7\ISFORM\isform.c на прилагаемом к книге компакт-диске.

Рассмотрим наиболее важные фрагменты исходного текста этого модуля.

В области глобальных переменных мы определили массивы для хранения идентификатора пользователя szUserID, его пароля szUserPassword, прав szUserRights, а также текста сообщения об ошибках sErrMsg (если они возникнут при обращении расширения к базе данных):

char szUserID[80];
char szUserPassword[80];
char szUserRights[80];
char sErrMsg[8000];

Исходный текст функции GetExtensionVersion никаких особенностей не имеет и полностью аналогичен исходному тексту, примененному нами во всех примерах расширений ISAPI.

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

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

CHAR szBuff[4096];
CHAR szTempBuf[4096];
char * szPtr;
char * szParam;

В начале работы мы помещаем нулевое значение в поле dwHttpStatusCode блока ECB, а затем записываем в буфер szBuff заголовок HTTP и начальный фрагмент документа HTML, формируемого динамически:

lpECB->dwHttpStatusCode = 0;

wsprintf(szBuff, "Content-Type: text/html\r\n\r\n"
  "<HTML><HEAD><TITLE>Права сотрудника</TITLE></HEAD>\n"
  "<BODY BGCOLOR=#FFFFFF><H2>Права сотрудника</H2>\n");

Данные, отправленные браузером, копируются в буфер szTempBuf:

lstrcpyn(szTempBuf, lpECB->lpbData, lpECB->cbAvailable + 1);
szTempBuf[lpECB->cbAvailable + 1] = '\0';

Эти данные затем перекодируются функцией DecodeStr:

DecodeStr(szTempBuf);

Исходный текст этой функции мы рассматривали в разделах, посвященных программам CGI.

Далее мы запускаем цикл извлечения из принятых данных содержимого полей формы с идентификатором сотрудника и его паролем:

szTempBuf[lpECB->cbAvailable] = '&';
szTempBuf[lpECB->cbAvailable + 1] = '\0';
 
for(szParam = szTempBuf;;)
{
  szPtr = strchr(szParam, '&');
  if(szPtr != NULL)
  {
     *szPtr = '\0';
     DecodeStr(szParam);
     GetParam(szParam);

     szParam = szPtr + 1;
     if(szParam >= (szTempBuf + lpECB->cbAvailable))
       break;
  }
  else
     break;
}

Здесь используется техника, с которой мы познакомили Вас в исходных текстах программ CGI. Она предполагает применение функций DecodeStr и GetParam.

Далее наша программа добавляет в буфер выходного документа HTML идентификатор пользователя и пароль:

strcat(szBuff, "User: ");
strcat(szBuff, szUserID);
strcat(szBuff, "<br>Pasword: ");
strcat(szBuff, szUserPassword);
strcat(szBuff, "<BR>");

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

Затем наше расширение ISFORM вызывает функцию get_manager_table, передавая ей идентификатор, пароль, а также указатель szUserRights на буфер, в который эта функция должна записать права пользователя:

if(!get_manager_table(szUserID, szUserPassword, szUserRights))
{
  strcat(szBuff, "<br>Rights: ");
  strcat(szBuff, szUserRights);
  strcat(szBuff, "<BR>");
}
else
{
  strcat(szBuff, "<h2>Произошла ошибка</h2>");
  strcat(szBuff, sErrMsg);
}

Если функция get_manager_table выполнила обращение к базе данных без ошибок, она возвращает нулевое значение. Наше приложение при этом добавляет в выходной буфер права пользователя, извлеченные из таблицы managers базы данных BookStore.

При возникновении ошибок в выходной буфер копируется содержимое строки сообщения об ошибке sErrMsg.

Далее в буфер записывается завершающий фрагмент динамически формируемого документа HTML, после чего содержимое буфера отправляется браузеру посетителя при помощи функции ServerSupportFunction:

strcat(szBuff, "</BODY></HTML>");  

if(!lpECB->ServerSupportFunction(lpECB->ConnID,
  HSE_REQ_SEND_RESPONSE_HEADER, NULL, NULL,
  (LPDWORD)szBuff))
{
  return HSE_STATUS_ERROR;
}

lpECB->dwHttpStatusCode = 200;
return HSE_STATUS_SUCCESS;

Исходный текст функции get_manager_table определен в файле odbcparam.c, исходный текст которого приведен в листинге 7-14.

Листинг 7-14 хранится в файле ch7\ISFORM\odbcparam.c на прилагаемом к книге компакт-диске.

Функция get_manager_table во многом аналогична функции main консольной программы ODBCPARAM, о которой мы рассказывали в предыдущей главе нашей книги, поэтому мы поговорим об отличиях.

Приложение ISFORM написано на языке С, а не С++, поэтому мы отказались от использования библиотеки шаблонов STL и типа данных string.

Сообщение об ошибке записывается в переменную sErrMsg, объявленную в файле odbcparam.c как extern:

extern char sErrMsg[8000];

Прототип функции get_manager_table приведен ниже:

int get_manager_table(char* szUserID, char* szUserPassword,
  char* szRights);

Через первый параметр ей передается указатель на строку идентификатора пользователя, через второй — указатель на строку пароля, а через третий — указатель на буфер, куда функция get_manager_table должна записать результат своей работы (права пользователя).

Далее функция get_manager_table выполняет инициализирующие действия, необходимые для работы с источником данных через интерфейс ODBC и создает соединение с этим источником:

rc = SQLAllocHandle(SQL_HANDLE_ENV, NULL, &hEnv);
rc = SQLSetEnvAttr(hEnv, SQL_ATTR_ODBC_VERSION,
     (SQLPOINTER)SQL_OV_ODBC3, SQL_IS_INTEGER);
rc = SQLAllocHandle(SQL_HANDLE_DBC, hEnv, &hDbc);

rc = SQLConnect(hDbc,
  szDSN,      (SWORD)strlen((const char*)szDSN),
  szUserName, (SWORD)strlen((const char*)szUserName),
  szPassword, (SWORD)strlen((const char*)szPassword));

Здесь мы убрали обработку ошибок, выполняемую функциями GetErrorMsg и GetErrorMsgConn.

На следующем этапе функция get_manager_table выполняет «привязку» параметров и вызов хранимой процедуры ManagerLogin:

rc = SQLAllocHandle(SQL_HANDLE_STMT, hDbc, &hStmt);
strcpy((char*)szAdminName, szUserID);

rc = SQLBindParameter(hStmt, 1, SQL_PARAM_INPUT, SQL_C_CHAR,
  SQL_CHAR, 50, 0, szAdminName, 51, &cbAdminName);

strcpy((char*)szAdminPass, szUserPassword);

rc = SQLBindParameter(hStmt, 2, SQL_PARAM_INPUT, SQL_C_CHAR,
  SQL_CHAR, 50, 0, szAdminPass, 51, &cbAdminPass);

rc = SQLBindParameter(hStmt, 3, SQL_PARAM_OUTPUT, SQL_C_CHAR,
  SQL_CHAR, 16, 0, szAdminRights, 51, &cbAdminRights);

rc = SQLExecDirect(hStmt,
  (unsigned char*)"{call ManagerLogin(?,?,?)}", SQL_NTS);

Значения входных параметров при этом копируются из параметров szUserID и szUserPassword функции get_manager_table.

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

strcpy(szRights, szAdminRights);

SQLFreeHandle(SQL_HANDLE_STMT, hStmt);
SQLDisconnect(hDbc);
SQLFreeHandle(SQL_HANDLE_DBC,  hDbc);
SQLFreeHandle(SQL_HANDLE_ENV,  hEnv);

Далее функция освобождает полученные ей ресурсы и возвращает управление.

Исходные тексты функций GetErrorMsgConn и GetErrorMsg аналогичны исходным текстам, использованным в программе ODBCPARAM. Мы внесли в них небольшие изменения: теперь сообщение об ошибке будет отображаться не в окне консольной программы, а в документе HTML. Кроме того, вместо переменной класса string, определенного в библиотеке шаблонов STL, мы использовали здесь обычные функции стандартной библиотеки С.

Обращение к базе данных в отдельном потоке

В предыдущем разделе для запуска хранимой процедуры мы обращались к функциям интерфейса ODBC непосредственно из функции HttpExtensionProc. Однако этот способ, хотя и работает, обладает одним существенным недостатком. Этот недостаток связан с тем способом, которым сервер Microsoft Internet Information Server обрабатывает запросы, получаемые от браузеров посетителей через протокол HTTP.

Для таких запросов сервер IIS создает пул идентификаторов потоков, обрабатывающих запросы. Если все потоки из этого пула заняты или находятся в состоянии ожидания, сервер отвергает вновь поступающие запросы.

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

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

Исходный текст главного модуля, измененный для использования отдельного потока, мы привели в листинге 7-15.

Листинг 7-15 хранится в файле ch7\ISFORM_THREAD\isform.c на прилагаемом к книге компакт-диске.

Рассмотрим внесенные изменения.

В области глобальных переменных мы определили переменные dwThreadID и g_dwThreadCount:

DWORD dwThreadID;
DWORD g_dwThreadCount = 0;

Первая из них предназначена для хранения идентификатора потока, создаваемого для обращения к базе данных, а вторая служит счетчиком потоков.

Формирование буфера выходного документа HTML выполняется в два приема. Вначале функция HttpExtensionProc записывает в него начальный фрагмент документа без заголовка:

wsprintf(szBuff,
  "<HTML><HEAD><TITLE>Права сотрудника</TITLE></HEAD>\n"
  "<BODY BGCOLOR=#FFFFFF><H2>Права сотрудника</H2>\n");
  . . .

  strcat(szBuff, "User: ");
  strcat(szBuff, szUserID);
  strcat(szBuff, "<br>Pasword: ");
  strcat(szBuff, szUserPassword);
  strcat(szBuff, "<BR>");

Далее функция HttpExtensionProc запускает функцию ThreadProc в отдельном потоке, передавая ей в качестве параметра указатель на блок ECB:

CreateThread(NULL, 0, ThreadProc, lpECB, 0, &dwThreadID);

Для запуска используется функция CreateThread. После запуска потока эта функция сразу же возвращает управление. Далее функция HttpExtensionProc увеличивает на единицу счетчик запущенных потоков (который Вы можете использовать для статистики), а затем возвращает управление:

InterlockedIncrement(&g_dwThreadCount);
return HSE_STATUS_PENDING;

Важно, что при этом функция HttpExtensionProc, возвращая значение HSE_STATUS_PENDING, не закрывает сеанс, а оставляет его открытым. В результате сеанс взаимодействия браузера посетителя в сервером остается незавершенным, несмотря на то, что функция HttpExtensionProc закончила свою работу.

Завершение сеанса при этом возлагается на функцию ThreadProc, работающую в отдельном потоке.

Получив управление, эта функция вызывает функцию get_manager_table, выполняющую обращение к базе данных:

if(!get_manager_table(szUserID, szUserPassword, szUserRights))
{
  strcat(szBuff, "<br>Rights: ");
  strcat(szBuff, szUserRights);
  strcat(szBuff, "<BR>");
}
else
{
  strcat(szBuff, "<h2>Произошла ошибка</h2>");
  strcat(szBuff, sErrMsg);
}
strcat(szBuff, "</BODY></HTML>");  

После вызова этой функции поток продолжает формирование содержимого буфера выходного документа szBuff.

На следующем этапе поток отправляет браузеру заголовок документа HTTP, вызывая для этого функцию ServerSupportFunction:

char szHeader[] =  "Content-type: text/html\r\n\r\n";

header_ex_info.pszStatus = "200 OK";
header_ex_info.pszHeader = szHeader;
header_ex_info.cchStatus = strlen("200 OK");
header_ex_info.cchHeader = strlen(szHeader);
header_ex_info.fKeepConn = FALSE;

success = lpECB->ServerSupportFunction(
  lpECB->ConnID, HSE_REQ_SEND_RESPONSE_HEADER_EX,
  &header_ex_info, NULL, NULL);

Далее остается только отправить браузеру содержимое буфера выходного документа HTML, что можно сделать при помощи функции WriteClient:

dwSize = strlen(szBuff);
lpECB->WriteClient(lpECB->ConnID, szBuff, &dwSize, 0);

Теперь мы можем завершить сеанс средствами функции ServerSupportFunction, передав ей во втором параметре константу HSE_REQ_DONE_WITH_SESSION:

lpECB->ServerSupportFunction(lpECB->ConnID,
 HSE_REQ_DONE_WITH_SESSION, NULL, NULL, NULL );

[Назад] [Содержание] [Дальше]