Базы данныхИнтернетКомпьютерыОперационные системыПрограммированиеСетиСвязьРазное
Поиск по сайту:
Подпишись на рассылку:

Назад в раздел

Простой web-чат простыми средствами Delphi 6 и Kylix 2. Часть 2

div.main {margin-left: 20pt; margin-right: 20pt} Простой web-чат простыми средствами Delphi 6 и Kylix 2. Часть 2.
Автор: Владимир Волков aka Shkoliar, shkoliar@kaluga.ru
Опубликовано: 16.05.2002
Оригинал: http://www.softerra.ru/review/program/17849/

Первое работающее приложение

Дальнейшее изложение подразумевает, что интегрированная среда разработки Delphi уже запущена у вас на компьютере, и WebModule, изображенный на рисунке 1.1 уже создан. Кроме того, подразумевается, что web-сервер IIS запущен у вас на адресе 127.0.0.1.

Выполните команду Project—>Options. В открывшемся диалоговом окне на странице Directories/Conditionals в строке Output directory: введите путь к каталогу, предназначенному для размещения скриптов на вашем web-сервере. (Для IIS это обычно C:InetpubScripts). В результате скомпилированные web-приложения будут сразу помещаться туда, где они могут быть выполнены web-сервером. Переименуйте WebModule1, присвоив в окне Objects Inspector его свойству Name значение wbmdlTest. Создайте в папке Projects папку web_test, и сохраните в ней текущий проект с именем test, файл Unit1.pas переименуйте при сохранении в web_test.pas. Если вы сейчас выполните компиляцию, вы получите исполняемый файл test.exe, который можно будет запустить, но обращение к которому через web-сервер возвратит ошибку следующего вида (см. рис. 1.3):


Рисунок 1.3

Возникновение такой ошибки вполне естественно. Дело в том, что объект TWebResponce, из свойств которого беруться заголовки HTTP, не был создан. Для создания объекта TWebResponce необходимо, чтобы в модуле web-приложения присутствовал хотя бы один производитель контента (о них речь пойдет ниже), либо была вызвана явным образом хотя бы одна стандартная процедура, использующая этот объект. Для того, чтобы при вызове web-приложения, оно выполнило какое-либо действие, это действие нужно создать и описать. Для этого нужно вызвать на экран Action Editor. Выполните двойной щелчок левой клавишей мыши, либо щелкните правой клавишей мыши внутри окна модуля wbmdlTest, или в Инспекторе объектов для wbmdlTest выберите кнопку  в поле значений свойства Actions. На экран будет выведено окно Action Editor (рис. 1.4):


Рисунок 1.4

Добавление новых действий производится щелчком мыши на кнопке . После того, как новый элемент добавлен в коллекцию, необходимо выбрать его (щелкните мышью по строке WebActionItem1 внутри окна Action Editor). В окне Objects Inspector отобразятся свойства этого элемента. Установите свойствa Default и Enabled в True, свойству Pathinfo присвойте значение first. Установка свойства first для Pathinfo означает, при наборе в строке адреса броузера значения: http://…./test.exe/first запрос будет передан на обработку именно этому элементу. Свойство Default установленное в True обязывает web-приложение передавать на обработку все запросы, для которых не нашлось элемента с соответствующим свойством Pathinfo в коллекции элементов. Независимо от количества элементов в коллекции, свойство Default может быть установлено в True только у одного из них.
Несмотря на то, что мы создали действие, которое должно быть выполнено web-приложением при выполнении любого запроса, обращенного к test.exe, печальная ситуация с отсутствием объекта TWebResponce и сообщением об ошибке CGI не изменилась. Вы можете проверить это, скомпилировав проект, и набрав в строке адреса броузера: http://127.0.0.1/Scripts/test.exe/first. Напишите для WebActionItem1 обработчик события OnAction:
procedure TwbmdlTest.wbmdlTestWebActionItem1Action (Sender: TObject; Request:TWebRequest; Response:TWebResponse; var Handled: Boolean); begin Response.Content:='test text'; end;Скомпилируйте приложение и запустите его, набрав в строке адреса броузера: http://127.0.0.1/Scripts/test.exe/first. Ваше web-приложение заработало, выдав на экран броузера строчку "test text". Это, конечно, не достижение. Такого же результата вы могли добиться, поместив в корневой каталог сервера простой текстовый файл, содержащий этот текст, и вызвав его по ссылке на имя файла. Смысл всей проделанной работы — в понимании того, как взаимодействуют свойства объекта TWebModule. Добавьте еще одно действие в коллекцию WebActionItems, присвойте его свойству PathInfo значение dinamic и напишите для его события OnAction следующий обрабочик:
procedure TwbmdlTest.wbmdlTestWebActionItem2Action (Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); begin Response.Content:='Местное время: '+ TimeToStr(Time); end;Скомпилированный проект теперь имеет два действия и два значения PathInfo, вызывая с которыми web-приложение, мы будет наблюдать в броузере различную реакцию сервера. Адрес http://127.0.0.1/Scripts/test.exe/test по прежнему выводит на экран текст "test text", а вот ссылка на адрес http//127.0.0.1/Scripts/test.exe/dinamic выводит на экран броузера значение текущего системного времени, которое динамически изменяется. Это можно проверить, щелкая на кнопке «Обновить» броузера. Проверьте так же, что набор адреса скрипта без PathInfo производит то действие, свойство Default которого было установлено в true. Начинаем делать чат

Почему для демонстрации возможностей динамической генерации в качестве web-приложения я выбрал именно программу web-чата? Первая причина в том, что трудно подобрать какой-то другой тип интернет-сервиса, где страницы генерировались бы более динамическим образом. Именно многопользовальский web-чат практически невозможно создать, не обрабатывая информацию при помощи скрипта на стороне сервера. Вторая причина состоит в том, что именно чат внутри одного приложения позволяет продемонстрировать практически все возможности, которые предоставляет программисту Delphi при создании web-приложений. Третьей же причиной явилось то, что именно с web-чатов начиналось множество современных интернет-порталов. Поэтому, если будет желание и возможность, никто не мешает нам пройти «естественным» путем: построить web-чат, а затем вырастить его в интернет-портал с новостной страницей, рассылкой, форумами, страницами пользователей и возможностями поиска в базах данных.

Фреймы

Создать web-чат без фреймов можно. Но делать это не имеет смысла, поскольку пользователю придется открывать два окна броузера, в одном осуществлять ввод, а во втором читать общий чат. Поэтому фреймовая структура страницы — идеальная техника для создания web-чата. Для работы чата в общем контейнере должно быть как минимум два фрейма, один фрейм чата, другой фрейм — ввода текста. Если мы имеем дело с html-документом, то задача построения страницы с двумя фреймами реализуется простым кодом:

<HTML> <HEAD> <!-- frames --> <FRAMESET rows="*,55"> <FRAME name="Top" src="top.html"> <FRAME name="Bottom" src="bottom.html"> </FRAMESET> </HEAD> </HTML>

Таким образом, для реализации простейшей двухфреймовой структуры нам нужно иметь три html-документа: контейнер, содержащий код, приведенный выше, и два фрейма, верхний и нижний, top.html и bottom.html.

Как реализовать этот код, используя технологию Delphi?

Когда мы создавали наше первое простейшее приложение, для генерации содержимого ответа web-сервера мы использовали прямое присваивание значения свойству Content объекта TWebResponce. В случае создания более сложных приложений, содержимое ответного сообщения генерируется специальными компонентами, производителями контента. На странице Internet палитры компонентов Delphi таких компонент, производителей контента, пять:

 — TPageProducer, производит строку HTML-команд, основываясь на заданном шаблоне  — TDataSetTableProducer, конструирует строку HTML-команд для представления таблицы из базы данных в виде таблицы HTML  — TDataSetPageProducer, производит строку HTML-команд, основываясь на заданном шаблоне путем подстановки в шаблон значений извлеченных из полей базы данных  — TQueryTableProducer, конструирует строку HTML-команд для представления результата запроса к базе данных в виде таблицы HTML. Работает с компонентом TQuery  — TQueryTableProducer, конструирует строку HTML-команд для представления результата запроса к базе данных в виде таблицы HTML. Работает с компонентом TSQLQuery

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

В Delphi выполните команду File—>New—>Other, и затем в диалоговом окне New Items на странице New выберите ярлык мастера Web Server Application. В открывшемся диалоговом окне выберите тип приложения CGI stand-alone executable. Переименуйте WebModule1, присвоив в окне Objects Inspector его свойству Name значение wbmdlChat. Созданный новый проект сохраните в папке web_chat с именем chat.dpr. Web-модулю проекта при сохранении присвойте имя web_chat.pas. В контейнер wbmdlChat поместите три компонента TPageProducer. Задайте для них имена: pgprdcrBase, pgprdcrChat и pgprdcrInput. Создайте для wbmdlChat три элемента WebActionItems и установите для них свойства такими, как это указано на рис. 1.5:


Рисунок 1.5

Контейнером для фреймовой структуры будет pgprdcrBase. Для того, чтобы PageProducer сгенерировал заданное содержимое, это содержимое можно поместить в html-файл, и затем указать имя этого файла в свойстве HTMLFile. Вторым способом задать шаблон html-документа для PageProducer является прямой ввод html-кода в поле свойства HTMLDoc при помощи String List Editor. Вызовите на экран окно редактора строк щелчком на кнопке в поле значений свойства HTMLDoc pgprdcrBase и введите туда следующий код:
<HTML> <HEAD> <!-- frames --> <FRAMESET rows="*,55"> <FRAME name="Chat" src=" 4758/Chat"> <FRAME name="Input" src=" 4758/Input"> </FRAMESET> </HEAD> </HTML> Для свойствa HTMLDoc pgprdcrChat при помощи String List Editor введите значение Chat frame. Для свойствa HTMLDoc pgprdcrInput при помощи String List Editor введите значение Input frame. Скомпилируйте приложение и запустите его, введя в строке броузера адрес: http://127.0.0.1/Scripts/chat.exe/Base. Результат должен быть таким, как на рисунке 1.6:


Рисунок 1.6

Это конечно же еще не чат. Это только фреймовая структура, которая сгенерирована нашим web-приложением. Для того, чтобы эта фреймовая структура стала похожа на чат, ее нужно научить делать две основные вещи: принимать текстовый ввод от клиента из Input frame, встраивать этот текст в общую страницу чата, и периодически обновлять информацию в фрейме Chat frame, для того, чтобы увидет свой собственный введенный текст и сообщения, отправленные в чат другими пользователями.

Первый крик малютки

Для CGI-приложения передача информации от одного действия к другому составляет определенную сложность. Дело в том, что когда клиент отправляет текстовую строку на сервер, сервер вызывает web-приложение (в нашем случае это chat.exe) передает ему обработку запроса. Web-приложение принимает запрос, обрабатывает его, вызывая для обработки действие, указанное в PathInfo (в нашем случае это Input), после чего завершает свою работу, уничтожая все созданные им во время работы динамические объекты. Отсылка информации с сервера в Chat frame на компьютере клиента производится ДРУГИМ экземпляром web-приложения, которое ничего не знает о присланной текстовой строке. Наиболее простым, постоянно существующим объектом, доступным для всех экземпляров web-приложения является файл (мы не рассматриваем в данный момент работу web-приложения в режиме, когда информация хранится в базе данных, это будет сделано позже). Таким образом, словесное описание работы чата должно выглядеть следующим образом:

При первой загрузке чата, клиент обращается по адресу http://127.0.0.1/Scripts/chat.exe/Base. В результате этого запроса запускается приложение chat.exe, выполняет действие Base, передает через сервер клиенту первый html-документ, содержащий фреймовую структуру, и завершает свою работу. Клиент, загрузив фреймовую структуру, обращается к ссылкам внутри нее в поисках информации, откуда загружать содержимое фреймов. Первой он обнаруживает ссылку на фрейм Chat, и отправляет на сервер запрос по адресу ../chat.exe/Chat. Следующая ссылка на второй фрейм, Input, так же отправляется с запросом на сервер вслед за первой с адресом ../chat.exe/Input. Сервер принимает эти запросы, и поочередно запускает два экземпляра приложения chat.exe, передавая им эти два запроса. Первый экземпляр chat.exe, выполняющий действие Chat, должен передать клиенту содержимое страницы чата в момент прихода запроса. Второй экземпляр chat.exe, выполняющий действие Input, должен передать клиенту форму ввода. Как мы уже говорили выше, регулярно обновляющийся контент страницы чата должен существовать на сервере постоянно, в виде файла. Это значит, что производитель контента pgprdcrChat, обслуживающий действие Chat, должен быть подключен к файлу. А вот производитель контента pgprdcrInput может хранить свой содержимое в свойстве HTMLDoc.

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

Создайте при помощи блокнота в каталоге Scripts текстовый файл chat_log.html Содержимое файла должно быть следующим:
<html> <head> <METAHTTP-EQUIV="Refresh" CONTENT="5; URL=../chat.exe/Chat" > <body> В качестве содержимого свойства HTMLDoc производителя контента pgprdcrInput введите следующий код:
<html> <form name="form1" method="post" action="../chat.exe/Input"> <input type="text/text" name="textfield" size=80> <input type="submit" name="Submit" value="Submit"> </form> </html> Напишите две следующие процедуры, для событий OnCreate и OnDestroy объекта wbmdlChat, создающие и уничтожающие объект типа TStringList, необходимый при размещения в памяти файла chat_log.html для его модификации:
procedure TwbmdlChat.WebModuleCreate(Sender: TObject); begin //создаем объект ChatStrings сразу после созданием модуля ChatStrings := TStringList.Create; end; procedure TwbmdlChat.WebModuleDestroy(Sender: TObject); begin //уничтожаем объект ChatStrings непосредственно перед //уничтожением модуля ChatStrings.Free; end;Не забудьте о том, что переменная ChatStrings должна быть объявлена в соответствующем разделе кода: var wbmdlChat: TwbmdlChat; ChatStrins: TStrings; Для того, чтобы посланная из формы строка, была воспринята web-приложением и добавлена к файлу chat_log.html, напишите обработчик события OnAction для элемента Input:
procedure TwbmdlChat.wbmdlChatInputAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); var st: String; begin //загружаем содержимое файла chat_log.html в коллекцию //строк ChatStrings ChatStrings.LoadFromFile('chat_log.html'); //формируем строку пользовательского ввода, полученную от //нижнего фрейма. Функция HTTPDecode преобразует полученную //строку в читаемый вид. Если вы хотите посмотреть, каков //нечитаемый вид, то попробуйте удалить эту функцию, //оставив вместо нее передаваемое ей значение, то, что //находится в круглых скобках. Тэги <b> и </b> делают //выводимый на страницу текст полужирным, это облегчает его //восприятие. st:='<b>'+ HTTPDecode(Request.ContentFields.Values['textfield']) +'</b>'; //Вставляем сформированную строку в коллекцию строк на //четвертую от начала позицию. Индекс имеет значение 3 //потому что отсчет начинается с нуля. ChatStrings.Insert(3, st+'<BR>'); //записываем результат обратно в файл ChatStrings.SaveToFile('chat_log.html'); end; Наконец, после того, как файл обработан, и в него вставлена строка, посланная формой нижнего фрейма, было бы неплохо загрузить этот файл в pgprdcrChat и закрыть тэги html. Internet Explorer спокойно пропускает такие ошибки, в то время, как Netscape Navigator очень щепетильно относится к синтаксису html, и может дать сбой, если обнаружит на странице незакрытые тэги. Напишите обработчик события BeforeDispatch для wbmdlChat:
procedure TwbmdlChat.WebModuleBeforeDispatch (Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); begin //загружаем содержимое файла chat_log.html в свойство //HTMLDoc производителя контента верхнего фрейма pgprdcrChat.HTMLDoc.LoadFromFile('chat_log.html'); //и закрываем основные тэги html pgprdcrChat.HTMLDoc.Append('</body>'); pgprdcrChat.HTMLDoc.Append('</html>'); end;

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

Страусенок начинает ходить

Ну а кто же он еще, наш чат младенческого возраста? Неуклюжий и шатающийся страусенок. Сейчас мы будем учить его ходить. Для начала, как я и обещал, ограничим разрастание файла chat_log.html количеством строк 20. Для этого в код обработчика события OnAction элемента Input добавим еще одну строку:

procedure TwbmdlChat.wbmdlChatInputAction(Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); var st: String; begin … ChatStrings.Insert(4, st1+'<BR>'); //пока общее количество строк в наборе больше 20, удаляем //строку в позиции 20 while ChatStrings.Count > 20 do ChatStrings.Delete(19); … end;

Теперь в файл chat_log.html всегда будет записываться 20 строк, соответственно, на экране будет отображаться 17 строк, потому что первый 3 строки, записываемые в файл не отображаются.

Следующая задача — идентифицировать пользовательский ввод. То есть добиться того, чтобы в начале каждой фразы в чате выводилось имя пользователя, который ее «сказал». Для решения этой задачи во-первых, создадим еще одну страницу, вход в чат с заданием имени пользователя. На следующем этапе на эту страницу можно будет добавить задание и других параметров настройки (количества выводимых на экран строк, времени перезагрузки верхнего фрейма, защиту имени паролем, выбор цвета, и т. д.) Во-вторых, нам нужно будет решить задачу передачи и сохранения имени пользователя, а так же идентификации сообщения.

Добавте к модулю wbmdlChat еще один компонент типа TPageProducer. Тут же переименуйте его в pgprdcrWelcome. При помощи String List Editor заполните значение его свойства HTMLDoc следующим кодом:
<html> <head> <title>Flying hedgehoggy </head> <body bgcolor="#FFFFFF" text="#000000"> <p><b><font size="7" color="#FF6666">Flying hedgehoggy </font></b></p> <p><b><font size="7"> Cool chat </font></b></p> <form name="form1" method="post" action="../chat.exe/Base"> <p><font size="5"> Input your name and welcome: </font> <input type="text/html" name=name> <input type=submit name=Submit value="Click it!">

</form> </body> </html> Добавьте к коллекции действий еще один элемент, переименуйте его в Welcome, установите его свойство Producer равным pgprdcrWelcome, свойству PathInfo присвойте значение Welcome, и, наконец, установите в True его свойство Default. Как вы поняли, в п. 1 мы сформировали еще одну страницу, которая содержит форму с одним полем — полем ввода имени. Проблема состоит в том, что передать это имя мы должны нижнему фрейму Input, а форма при отправке сообщения вызвывает для исполнения (и это естественно) контейнер Base. Возникает вопрос, как передать введенное имя из фрейма Base фрейму Input? Для передачи имени воспользуемся одним из наиболее интересных свойств производителей контента, а именно — прозрачными (транспарентными) html-тэгами. Введем в фрейм Base (в свойство HTMLDoc pgprdcrBase) транспарентный тэг <#custom> следующим образом: заменим строку <FRAME name="Input" src=" 4758/Input">строкой <FRAME name="Input" src="../chat.exe/Input?name=<#custom>">. Кроме этого, напишем обработчик для события OnHTMLTag для этого компонента (pgprdcrBase):
procedure TwbmdlChat.pgprdcrBaseHTMLTag (Sender: TObject; Tag: TTag; const TagString: String; TagParams: TStrings; var ReplaceText: String); begin //Транспарентный тэг в тексте контейнера Base перед //отправкой клиенту заменяется значением имени //пользователя, полученным от формы Welcome. Это значение //передается в качестве параметра запроса далее фрейму //Input ReplaceText:=Request.ContentFields.Values['name']; end; После того, как мы передали значение имени фрейму Input, нужно сделать так, чтобы этот фрейм хранил в себе переданное имя, и передавал его в качестве параметра каждый раз, когда мы отправляем сообщение на сервер. Для этого в код фрейма Input так же введем транспарентные тэги, дополнив его парой строк (добавленные строки выделены наклонным шрифтом):
<html> <form name=inp method=post action="../chat.exe/Input"> Hi, <#custom>! <input type="text" name="textfield" size=80> <input type="submit" name="Submit" value="Send"> <input type=hidden name=name value=<#custom>> </form> </body> </html>
Подготовив таким образом поля для хранения имени в фрейме, обеспечим его получение, запись в поле и дальнейшее хранение (передаваться оно будет теперь автоматически при каждой передаче введенного пользователем текста, в качестве значения скрытого поля формы). Напишите обработчик события OnHTMLTag для компонента pgprdcrInput: procedure TwbmdlChat.pgprdcrInputHTMLTag (Sender: TObject; Tag: TTag; const TagString: String; TagParams: TStrings; var ReplaceText: String); begin if Request.Method <> 'POST' //при первой загрузке фрейма значение имени берется из поля //запроса контейнера Base then ReplaceText:=Request.QueryFields.Values['name'] //во всех последующих случаях фрейм Input возвращает себе //то значение имени пользователя, которое сам же послал else ReplaceText:=Request.ContentFields.Values['name']; end; Остался последний штрих: обеспечить ввод имени пользователя перед текстом сообщения в фрейме Chat. Для это придется немного дописать обработчик события OnAction элемента Input (дописанный код выделен наклонным шрифтом):
procedure TwbmdlChat.wbmdlChatInputAction (Sender: TObject; Request: TWebRequest; Response: TWebResponse; var Handled: Boolean); var st: String; begin ChatStrings.LoadFromFile('chat_log.html'); //защищаемся от "пустого ввода", строка будет добавлена //только если она не пустая if Request.ContentFields.Values['textfield']<>'' then begin //Добавляем в строку имя пользователя, полученное от фрейма //Input st:='<b>'+HTTPDecode(Request.ContentFields.Values['name'])+'<font size=+1 color="#FF6666">></font>' + HTTPDecode(Request.ContentFields.Values['textfield'])+ '</b>'; ChatStrings.Insert(3, st+'<BR>'); end; ChatStrings.SaveToFile('chat_log.html'); end;

Все! Мы получили работающий чат. Наш страусенок пошел! Не будем обольщаться, на просторы Интернета это чудо рукоприкладства выпускать еще рано, но поболтать в нем с друзьями уже можно. Не забудьте предварительно перенастроить IIS так, чтобы ваш web-сервер стал виден во внешней сети, и изменить соответствующим образом URL чата. Пока еще за рамками обсуждения остались вопросы защиты имени паролем, задания цвета, частоты обновления, количества строк, возможность организации приватов, возможность добавлять обращения к собеседнику, щелкнув мышью на его имени, и множество других возможностей, которыми должен обладать современный web-чат.

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

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


Почитать на ту же тему на сайте «Софтерра»: Простой web-чат простыми средствами Delphi 6 и Kylix 2. Часть 1.(http://www.softerra.ru/review/program/17829/)

Архитектуры и типы web-приложений, базовые элементы.


Вниманию вебмастеров: использование данной статьи возможно только в соответствии
с правилами использования материалов сайта «Софтерра» (http://www.softerra.ru/site/rules/)


  • Главная
  • Новости
  • Новинки
  • Скрипты
  • Форум
  • Ссылки
  • О сайте




  • Emanual.ru – это сайт, посвящённый всем значимым событиям в IT-индустрии: новейшие разработки, уникальные методы и горячие новости! Тонны информации, полезной как для обычных пользователей, так и для самых продвинутых программистов! Интересные обсуждения на актуальные темы и огромная аудитория, которая может быть интересна широкому кругу рекламодателей. У нас вы узнаете всё о компьютерах, базах данных, операционных системах, сетях, инфраструктурах, связях и программированию на популярных языках!
     Copyright © 2001-2024
    Реклама на сайте