ГЛАВА 15


Создание Web-серверов

 

Общие замечания

Многие вопросы создания Интернет-систем были изложены в предыдущих главах. Настало время познакомиться с процессом создания собственно Web-серверов.

Серверы различного назначения могут быть реализованы на базе сведений о сокетах, описанных в гл. 13. В этой главе рассматривается процесс создания сервера, взаимодействующего с универсальным клиентом — браузером. Поскольку очень часто нет смысла вторгаться на сторону этого клиента и каким-либо образом модернизировать его, то для обмена данными будет использоваться распространенный протокол HTTP. Задача сервера состоит в получении клиентских сведений и, затем, отправке браузеру предварительно сгенерированного ответа. В отличие от базирующихся на технологии CGI серверных модулей, где вся ответственность за разбор клиентских данных ложилась на Web-сервер, эту работу должна проводить создаваемая программа. Данная программа будет также генерировать ответ.

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

Однако данный подход к созданию серверной части имеет ряд недостатков, главным из которых является повышенное требование устойчивости создаваемой программы к возникновению исключительных ситуаций. Дело в том, что если "зависнет" CGI-модуль, то это половина беды, т. к., скорее всего, работоспособность всего Web-сервера не будет нарушена, но если "зависнет" сам сервер, то "клиническая смерть" Web-узлу обеспечена.

 

Постановка задачи

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

Создадим сервер обмена сообщениями, проще говоря — чат. В этом чате должны быть реализованы следующие требования:

Анализ HTTP-заголовков, получаемых от клиента

В гл. 9, посвященной CGI, были отдельно рассмотрены HTTP-заголовки. Этот материал касался только серверной части, что было обусловлено использованием Web-сервера. Он берет на себя весь процесс разбора клиентского запроса с передачей результата своей работы в переменные окружения. Мы использовали эти переменные для доступа к интересующей нас информации. Теперь, создавая сервер, необходимо самостоятельно получить и обработать эти данные.

Чтобы получить информацию о том, в каком виде она передается от клиента к серверу, создадим вспомогательную программу, роль которой будет заключаться лишь в возвращении заголовков, получаемых от клиента, ему же, но в теле Web-страницы. Для этого создадим новый проект в Delphi под названием httphead. Для этого на обычную форму нужно поместить компонент TServerSocket, находящийся на вкладке Internet Component Palette.

Из всех параметров, установленных у помещенного на форму компонента по умолчанию, нужно изменить лишь номер порта. Портом по умолчанию для Web-служб является 80. Обратите внимание, что одновременная работа нескольких приложений на одном порте вызывает конфликтную ситуацию, поэтому если на компьютере работает другой Web-сервер, то на время отладки этого приложения его нужно отключить, либо присвоить порту другой номер. Вслед за определением номера порта, свойству Active используемого объекта — экземпляра класса TServerSocket, присваивается значение true.

Следующим этапом в создании вспомогательного приложения является обработка событий OnClientRead И OnClientWrite. Определим глобальную переменную dheaders в секции Var строкового типа. Теперь осталось получить от клиента заголовки и переправить их в содержание ответа. Процедура получения данных от клиента приведена в листинге 15.1.

 Листинг 15.1. Получение данных от клиента 

procedure TForml.ServerSocketlClientRead(Sender: TObject;

Socket: TCustomWinSocket); begin

dheaders: = (Socket.ReceiveText) ;

  end;

После того как содержимое HTTP-заголовка занесено в переменную dheaders, можно посылать ответ клиенту, как показано в листинге 15.2.

 Листинг 15.2. Процедура отправки данных клиенту 

procedure TForml.ServerSocketlClientWrite(Sender: TObject;

Socket: TCustomWinSocket); begin

Socket.SendTextCHTTP/1.1 200 Ok'fl3#10); Socket.SendText('Content-Type: text/html'#13110); Socket.SendText(''#13#10); Socket.SendText('<HTML>'); Socket.SendText(clheaders); Socket.SendText('</HTML>') ; 

end;

 Замечание 

Обратите внимание, что отправка HTTP-заголовков клиенту осуществляется в соответствии с правилами, изложенными в гл. 9, в разделе, посвященном прямому выводу информации.

Запустив созданное приложение, браузер, а затем, указав в адресной строке последнего localhost, на экране можно увидеть искомую информацию (рис. 15.1).

Рис. 15.1. Результат работы созданного вспомогательного сервера Описание полей посылаемого браузером запроса приведено в табл. 15.1.

Таблица 15.1. Описание данных, отправленных клиентом Web-серверу

Заголовок поля

Описание

'GET/

Содержит название метода запроса, которым воспользовался клиент. После значка / содержится дополнительная информация об адресе запрашиваемой Web-страницы, либо название команды, предусмотренной сервером

НТТР/1.1

Содержит название и версию используемого протокола

Accept: */*

Содержит тип MIME-данных, которые готов принять браузер в качестве ответа сервера

Accept-Language : ru

Содержит основной язык браузера

Accept-Encoding

Содержит поддерживаемые форматы упаковки файлов

User-Agent

Содержит тип агента пользователя (браузера)

Host

Содержит адрес сайта

Connection

Содержит тип соединения, которое поддерживает клиент. Этот заголовок, как правило, используется для указания того, что браузер поддерживает долгоживущие соединения

В этой таблице указаны стандартные заголовки. Для анализа данных, которые пересылаются при отправке параметров от клиента, удобно пользоваться программой HTTPDemo, поставляемой вместе в Delphi в качестве примера. Она позволяет передавать GET- и розт-запросы серверу и отображать его ответы. Также можно создать простейшую Web-страницу с формами для отправки данных, с помощью которых производится анализ содержимого клиентских HTTP-заголовков. Так и поступим (листинг 15.3).

 Листинг 15.3. Исходный код вспомогательной Web-страницы 

<HTML>

<P><FORM method=POST action="http://localhost">

<INPUT type=text name="mymessage">

<BR>

<INPUT type=Submit name="Submut" Уа1ие="0тправить">

</FORM>

<PXFORM method=GET action="http://localhost">

<INPUT type=text name="mymessage">

<INPUT type=Submit name="Submut" Уа1ие="0тправить">

</FORM>

</HTML>

Если текстовые поля заполнить значением "Сообщение", а затем последовательно воспользоваться обеими кнопками отправки, то отправляемые клиентом заголовки примут вид, приведенный в листингах 15.4 и 15.5.

Листинг 15.4. HTTP-заголовки, формируемые при передаче данных от клиента методом POST

POST / HTTP/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/msword, */* Accept-Language: ru Content-Type: application/x-www-form-urlencoded Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) Host: localhost Content-Length: 72 Connection: Keep-Alive mymessage=%Dl%EE%EE%El%F9%E5%ED%E8%E5

&Submut=%CE%F2%EF%FO%EO%E2%E8%F2%FC

Листинг 15.5. HTTP-заголовки, формируемые при передаче данных от клиента методом GET

GET /?mymessage=%Dl%EE%EE%El%F9%E5%ED%E8%E5&Submut=

%CE%F2%EF%FO%EO%E2%E8%F2%FC HTTP/1.1 Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, application/vnd.ms-excel, application/msword, */* Accept-Language: ru Accept-Encoding: gzip, deflate User-Agent: Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0) Host: localhost Connection: Keep-Alive

Приведенные листинги отличаются от текста на Web-странице, изображенной на рис. 15.1. Листинг 15.5 содержит дополнительные сведения СЕТ-запроса, указанные после вопросительного знака. Если бы клиент указал дополнительный адрес запрашиваемого документа, то путь к нему разместился бы между символом / и вопросительным знаком. Правила кодировки информации при передаче через Интернет и в СЕТ-запросах в частности, подробно изложены в гл. 9, поэтому здесь мы не будем останавливаться на них.

В методе POST, после указания названия этого метода, как и в СЕТ-случае, присутствует символ /, после которого должен следовать адрес запрашиваемого документа, либо команда серверу. Поскольку в Web-странице в поле action формы был указан только сервер, то данный параметр опущен. Кроме прочих заголовков, здесь в параметре Content-Length указывается количество символов, составляющих розт-запрос. Само тело запроса является последним набором данных, передаваемых от клиента к серверу.

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

 

Обзор структуры сервера

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

Реализация Web-сервера

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

На форму нового приложения следует поместить всего два компонента — serverSocket и NMURL. После того необходимо установить следующие свойства первого:

Name: MainSocket, Port: 80, Service: Http, Active: True.

Второй следует просто переименовать в TransiateMess.

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

 Листинг 15.6. Секция private определения класса TForml 

private

tmp, HTMLmessages: string; 

users: tstrings; 

refresh: booi;

Переменная tmp предназначена для обмена данными о содержимом ответа пользователю между процедурами. В HTMLmessages будет храниться банк посланий от клиентов, users используется в качестве массива, содержащего строки вида

IРАдресКлиента=ИмяПользователя

для идентификации клиентов. Обратите внимание, что объекты типа TstringList снабжены большим набором методов, осуществляющих управление данными такого вида.

Поскольку наш сервер должен посылать часть Web-страниц со специальным HTTP-заголовком, который указывает браузеру на необходимость обновления их содержимого через определенный интервал времени (см. в гл. 9 Pull-метод), то режим отправки ответа пользователю и будет определяться состоянием переменной refresh.

В начале работы программы необходимо создать объект users и присвоить переменной refresh значение по умолчанию (листинг 15.7).

 Листинг 15.7. Начальные действия в программе 

procedure TForml.FormCreate(Sender: TObject); begin

users:=TstringList.Create; refresh:=false; end;

Вслед за совершением начальных операций программа готова к приему данных (листинг 15.8).

 Листинг 15.8. Процедура чтения данных от клиента 

procedure TForml.MainSocketClientRead(Sender: TObject;

Socket: TCustomWinSocket); var direction, content:string; begin

content:=(Socket.ReceiveText);

// Здесь все содержимое HTTP-запроса передано в переменную content if pos('GET',content)=1 then

// Здесь происходит определения метода запроса (передачи данных // на сервер). В дальнейшем идет обработка метода POST и GET. direction:=copy(content, (pos('GET1,content)+5),

(pos('HTTP',content)-!)-(pos('GET',content)+5))

 else 

direction:=copy(content, (pos('POST1,content)+6),

(pos('HTTP',content)-1)-(pos('POST',content)+6)); // В этом фрагменте кода было обеспечено извлечение данных // о запрашиваемом динамически формируемом документе.

if direction = 'registration1 then

// Здесь происходит передача страницы входа на сервер в переменную tmp.

 begin

tmp:='<HTML>'+

'<TITLE> Вход на сервер </TITLE>'+

'<Н2> Вход на сервер </Н2>'+

'<DIV style="border-style: double; border-color: silver;'+

'width: 400px; height: 100px;'+ 'ALIGN: center; background-color: ttFODBF2">'+ '<FORM method=POST action="http://localhost/framesetl">'+ '<Р>Введите имя, под которым Вы хотите войти в систему.'+ '<INPUT type=text name="username">'+ '<P><INPUT type=Submit name="Submit" value="BoUTH">'+ '</FORM>'+ '</DIV>'+ '</HTML>'; 

end;

if direction = 'framesetl' then

// Генерация фреймсодержащей страницы — главного окна чата. begin

tmp:=content; content:=Copy(tmp,Pos{'username=',content)+9,

(Pos('&Submit=',content)-Pos('username=',content)-9)); // Выделение из полученных от клиента данных подстроки, содержащей имя // пользователя.

TranslateMess.InputString:=content; content:=TranslateMess.Decode; // Раскодировка веделенной информации (приведение к стандартному виду).

if users.lndexOfName(socket.RemoteAddress) = -1 then // Проверка — является ли клиент новым, или он уже находится в чате?

 begin

// Обработка ситуации, когда клиент является новым. Формируется // фреймсодержащая страница.

users.Add(socket.RemoteAddress+'='+content); tmp:='<HTML>'+

'<TITLE> Наш чат </TITLE>'+

'<FRAMESET rows="*,150">'+

1<FRAME src="http://localhost/messages" name="mess">'+ '<FRAME src="http://localhost/addingmessages"'+ 'name="adding">'+ '</FRAMESET>'+ '</HTML>'; 

end 

else

// Обработка ситуации двойного входа в чат. begin

tmp:='<HTML>'+

'<TITLE> Ошибка регистрации </TITLE>'+

'<Н4> Уважаемый пользователь! Ваш IP-адрес уже' +

'зарегистрирован в Чате.'-t-1<BR>Haui чат не позволяет одному пользователю'+

'обмениваться сообщениями ' + 'под несколькими псевдонимами</Н4>'+ '</HTML>'; end; end;

if direction = 'addingmessages' then

// Здесь генерируется страничка с формой для добавления нового сообщения. begin

tmp:='<HTML>'+

'<FORM Method=Post Action="http://localhost/messages" target="mess">'+ '<INPUT type=text name="message">'+

'<INPUT type="Submit" name="Submit" Уа1ие="Послать сообщение">'+ '</FORM>'+ '</HTML>';

  end;

if direction = 'messages' then

// Блок, отображающий список всех сообщений, а также добавление новых. begin content:=Copy(content,Pos('message=',content)+8,

(Pos('Submit=',content)-1-(Pos('message=',content)+ 8})) ; // Выделение нового послания.

TranslateMess.InputString:=content;

content:=TranslateMess.Decode;

if (length(HtmiMessages) = 0) and (length(content)=0) then

tmp:='<Р>Сообщений пока нет'

// Проверка — есть ли хоть одно послание или добавляется новое.

 else

 begin

refresh:=true;

// Установка режима передачи HTTP-заголовка для обновления страницы, tmp:='<message author="'+users.Values[socket.RemoteAddress] +' "><mbody>'+content+'</mbody></message>';

// Формирование XML-представления нового послания.

Xmltohtmlmessages(tmp); 

// Добавление нового послания к переменной, содержащей предыдущие.

tmp:=setskin;

 // Добавление стандартных HTML-элементов.

end;

 end;

  end;

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

Листинг 15.9. Реализация процедуры Xmltohtmlmessages 

procedure TForml.Xmltohtmlmessages(addingmess:string); begin

if length(copy(addingmess,(pos('<mbody>',addingmess)+7},

(pos('</mbody>',addingmess)-(pos('<mbody>',addingmess)+7))))>0 then begin

HtmlMessages:=

copy(addingmess, (pos('<mbody>',addingmess)+7) , (pos('</mbody>',addingmess)-

(pos('<mbody>',addingmess)+7)))+'<BR>'+HtmlMessages; // Здесь происходит выделение из параметра вызова процедуры // непосредственно сообщения и информации об авторе и добавление этих // данных в переменную HtmlMessages, содержащую все сообщения в формате // HTML.

HtmlMessages:='<SPAN class="name">'+ copy(addingmess,(pos('author=',addingmess)+8), (pos('">',addingmess)-(pos('author=',addingmess)+8)))+ ': <SPAN class="mess">'+HtmlMessages; end; end;

Далее, формируя Web-страницу, следует добавить в отправляемый HTML-код информацию о стиле и теги начала и конца документа. Эта операция реализована в процедуре setskin, код которой приведен в листинге 15.10.

Листинг 15.10. Добавление стилевых параметров в формируемую Web-страницу

function TForml.setskin:string; begin

if length(HtmlMessages) = 0 then HtmlMessages:='<Р>Сообщений пока нет';

Result:='<HTML>'+

'<STYLE type="text/ess">'#13#10+

'.name {color:red; font-weight:bold; font-style:italic; font-size: 12pt; }'#13#10+

'.mess {color:black; font-size:14pt;}'#13#10+

'</STYLE>'ttl3#10+Htmlmessages+'</HTML>';

  end;

После того как Web-страница готова, ее можно отправлять клиенту. К телу самой страницы, т. е. HTML-коду, следует добавить HTTP-заголовки, и передать эту информацию через сокетное соединение, как показано в листинге 15.11.

 Листинг 15.11. Процедура отправки HTTP-сообщения клиенту 

procedure TForml.MainSocketClientWrite(Sender: TObject;

Socket: TCustomWinSocket); 

begin

Socket.SendTextCHTTP/1.1 200 Ok'#13#10); if refresh = true then

// Поскольку данная процедура применяется при отправке всех страниц, то // нужно проверять — необходимо ли указывать браузеру обновлять страницу // через, например, 30 секунд. begin

refresh:=false;

Socket.SendText('Refresh:30; URL="http://localhost/messages"');

  end;

Socket.SendText('Connection: close'#13#10);

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

// Сочетание #13#10 является служебным символом, означающим перевод строки. // Его использование обусловлено необходимостью отделения HTTP-заголовков. Socket.SendText('Content-Type: text/html'#13#10); Socket.SendText('Ч13#10); Socket.SendText(tmp+#13#10); 

Socket.Close; 

Socket.Free;

  end;

Итак, после того, как в браузере, предварительно запустив созданную программу, набрать строку http://localhost/registration, будет загружена страница входа в чат, представленная на рис. 15.2.

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

Рис. 15.2. Страница регистрации на сервере

Рис. 15.3. Результат работы Web-сервера

Итак, мы создали сервер, основной задачей которого является предоставление возможности пользователям обмениваться сообщениями. Если на компьютере уже установлен Web-сервер, то программу можно использовать, указав номер сетевого порта, отличный от порта Web-сервера (например, 800), чтобы не возникало конфликтной ситуации. В этом случае, все ссылки нужно поменять, добавив после адреса, через двоеточие, номер порта. Например, http://localhost/registration переходит в http://localhost /registration: 800.