Изображение квадрата Дюрера

ООО АВТОМАТИКА плюс

Rambler's Top100

Рейтинг@Mail.ru

Разработка Web-приложений в Delphi с использованием технологии WebBroker

Версия 21.02.10

Создание простейшего CGI-приложения
Модели Web-приложений и их особенности
Создание CGI-приложения по технологии WebBroker
Установка CGI-приложения на сервер Apache
Разработка ISAPI-приложения для Internet Information Services
Разработка Apache Shared Module
Разработка CGI-приложения для публикации базы данных
Модуль извлечения файлов из POST-запроса
Алгоритм передачи файла браузеру
Сжатие HTML-страницы для экономии траффика
Выбор кодировки: WIN-1251 vs. UTF-8
Инструментарий Web-разработчика из разряда "must have" (должен иметь)
Заключение
Использованные источники

Статья ориентирована на начинающих Web-программистов, а также на тех, кто хочет ознакомиться в возможностями Delphi, касающихся разработки Web-приложений. Под термином "Web-приложение" я подразумеваю приложение HTTP-сервера (Apache, IIS и др.). В статье будут рассмотрены возможности Delphi 7 Enterprise. Полагаю, что все сказанное здесь будет верно и для Delphi 6 и для Delphi 2006 и для Delphi 2007.

Необходимо в начале разработки решить вопрос выбора HTTP-сервера, и выбрать модель построения вашего приложения. В настоящее время наибольшее распространение получили 2 HTTP-сервера: Internet Information Server (Services) (ISS) от Microsoft и HTTP-сервер (демон, служба) Apache. Мне приходилось работать с IIS на WinXP. IIS по умолчанию не был установлен. Для установки я выбрал Пуск / Настройка / Панель управления / Установка и удаление программ / Установка компонентов Windows / Internet Information Services. Программа установки потребовала диск с ОС Windows, и с помощью него я завершил установку.

После установки IIS требуется настроить виртуальный каталог для вашего сайта. Для этого нужно щелкнуть правой кнопкой мыши на иконке "Мой компьютер", выбрать "Управление", развернуть узлы "Службы и приложения", "Internet Information Services", "Веб-узлы". Щелкаем правой кнопкой на "Веб-узел по умолчанию" и выбираем "Создать / Виртуальный каталог", жмем "Далее", задаем псевдоним виртуального каталога "webtest" и жмем "Далее", выбираем каталог, в котором будут располагаться файлы нашего сайта (имя сайта мы только что указали - "webtest"). Укажем каталог "c:\inetpub\webtestdir" (предварительно его следует создать) и щелкаем "Далее", после этого задаем права доступа (сейчас я оставил все как есть, но если сайт работает с помощью ISAPI (DLL-библиотека) или CGI (PHP, Perl, CGI, EXE и т.д.), то нужно установить флаг "выполнение (например приложений ISAPI или CGI)", а также настроить параметр "Анонимный доступ". Жмем "Далее", затем "Готово". Дальше развернем узел "Веб-узел по умолчанию", и увидим, что там появился пункт "webtest". Щелкнем на нем провой кнопкой мыши и выберем пункт "Свойства". Откроется окно с множеством вкладок, позволяющее настроить все необходимые параметры для нашего сайта. Теперь убедимся, что IIS работает. Создадим в каталоге "c:\inetpub\webtestdir" файл index.htm (я просто создал такой файл с помощью блокнота, и скопировал в него содержимое первой попавшей под руку интернет-страницы (у вас под рукой находится то, что вы в данный момент читаете :-)). Сохраните файл index.htm, и в адресной строке Web-браузера наберите "http://localhost/webtest/". Страница открылась? Замечательно! IIS работает!
Сказанное верно для версии IIS под WinXP. Для других Windows этапы установки и настройки могут отличаться.

Создание простейшего CGI-приложения

Нам удалось настроить IIS на работу со статическими страницами. Теперь настало время рассмотреть технологию CGI. В простейшем случае CGI - это консольное приложение. Практически все современные языки программирования позволяют разрабатывать консольные приложения, а все популярные операционные системы позволяют такие приложения запускать. Запустите Delphi и выберите меню File / New / Other / Console Application. Сохраните файл проекта под именем cgitest.dpr. Замените содержимое файла cgitest.dpr на следующее:

  program cgitest;

  {$APPTYPE CONSOLE}

  {$E cgi}

  begin
    { TODO -oUser -cConsole Main : Insert code here }
    WriteLn('Content-Type: text/html');
    WriteLn;          

    WriteLn('Hello, World!<br>');
    WriteLn('This is simple cgi application!');
  end.  
  

Первой строкой приложение должно возвращать "Content-Type: text/html", а следующей должна следовать пустая строка (WriteLn;). Директива {$E cgi} определяет расширение исполняемого файла. Наш файл будет называться "cgitest.cgi". Укажите в опциях проекта (Project / Options / Directories/Conditionals / Output directory) каталог, в который будет размещен файл "cgitest.cgi". Укажите каталог "c:\inetpub\webtestdir\". Выполните компиляцию проекта и убедитесь, что файл "c:\inetpub\webtestdir\cgitest.cgi" присутствует.

В настройках IIS (см. выше) откройте свойства узла "webtest", перейдите на страницу "Виртуальный каталог". В поле "Разрешен запуск" выберите "Сценарии и исполняемые файлы". В поле "Защита" я на всякий случай выбрал пункт "Низкая (Процесс ISS)". Перейдите на вкладку "Безопасность каталога" и щелкните "Изменить...". Появится окно "Методы проверки подлинности". В поле "Анонимный доступ" вы увидите имя пользователя, под которым IIS будет запускать CGI-приложения. Сейчас там вы видите пользователя "IUSR_ComputerName". Эта учетная запись была создана автоматически при установке службы IIS. Ее тип: Гостевая учетная запись интернета. Ее описание: Встроенная учетная запись для анонимного доступа к IIS. Она относится к группе пользователей "Гости". Ей запрещен доступ к системным каталогам и к разделу HKEY_LOCAL_MACHINE системного реестра (вернее доступ есть, но только на чтение). В некоторых случаях это ограничение оказывается недопустимым, и приходится менять настройки анонимного доступа. Вместо пользователя "IUSR_ComputerName" я выбрал пользователя "Администратор". Предварительно мне пришлось добавить пароль для входа пользователя "Администратор". Сперва я из панели управления открыл "Учетные записи пользователей", щелкнул "Изменение входа пользователей в систему" и снял флаг "Использовать страницу приветствия". Далее щелкнул Пуск / Выход из системы / Смена пользователя, вошел под пользователем "Администратор", открыл окно "Учетные записи пользователей", щелкнул на учетной записи "Администратор", выбрал "Создание пароля" и задал пароль. Здесь же я вновь выбрал "Изменение входа пользователей в систему" и вернул флаг "Использовать страницу приветствия". Затем я вновь вошел под своей учетной записью, открыл окно свойств узла "webtest" и в окне "Методы проверки подлинности" выбрал пользователя "Администратор". Для этого в поле "Анонимный доступ" я щелкнул Обзор / Дополнительно / Поиск, выбрал из списка пункт "Администратор", щелкнул 3 раза "ОК", затем "Применить". С настройкой учетной у меня проблем не возникло, т.к. компьютер находится в "рабочей группе", а я вхожу в группу "Администраторы". Кстати, вместо пользователя "Администратор" вы могли бы выбрать любого другого пользователя с аналогичными правами, только предварительно следует создать для него пароль.

Теперь введите в адресной строке Web-браузера текст "http://localhost/webtest/cgitest.cgi/". У вас откроется HTTP-страница с текстом:
Hello, World!
This is simple cgi application! 

Наше приложение уже является полноценным приложением HTTP-сервера, несмотря на число строк кода (всего 4 функциональных строки кода), и размер выходного файла (всего 15 Кбайт). Огромное число Internet-сайтов построено именно по этой технологии, т.е. с помощью той или иной среды программирования создается консольное приложение, функциональность которого наращивается по мере необходимости. Кстати, различные интерпретаторы (Perl, PHP и др.) тоже являются CGI-приложениями (только они еще выполняют задачу по интерпретации скриптов).

При размещении Web-сайта на собственном сервере у вас есть выбор при разработке CGI-приложения. Вы можете разработать полноценное консольное CGI приложение, и добавить в него любою функциональность. Оно может иметь доступ к любым ресурсам серверного компьютера, может им управлять. Также вы можете разработать свое приложение в форме PHP- или Perl-скрипта, но в этом случае приложение сможет выполнить только действия, разрешенные интерпретатором. Например, интерпретатор PHP может разрешить доступ к любому файлу из директории, в которой расположен файл скрипта, но при этом запретить доступ к любой другой директории. В плане безопасности этот подход является более удобным, поэтому, если вы хотите разместить свой сайт на каком-нибудь интернет-сервере, предоставляющим услуги (платные или бесплатные) по размещению пользовательских сайтов, то HTTP-сервер скорее всего будет настроен лишь на работу со скриптовыми сайтами, а консольные CGI-приложения работать не будут. В этом случае изучайте скриптовые языки :). Однако нынешняя тенденция, при которой все больше и больше пользователей Интернета размещают Web-сервер на собственном компьютере, делает консольные CGI-приложения очень популярными.

Модели Web-приложений и их особенности

Технология WebBroker позволяет создавать кроссплатформенные приложения для HTTP-серверов с использованием технологий ISAPI/NSAPI, CGI executable, Apache Shared Module, а также для отладки (Web App Debugger Executable). Причем эта технология позволяет с наименьшими усилиями конвертировать проект из одного типа в другой. Например, при разработке Web-приложения использовался тип Web App Debugger Executable (позволяет использовать отладчик Delphi для трассировки Web-приложений). После окончания разработки можно преобразовать приложение в ISAPI/NSAPI, CGI executable или Apache Shared Module. Процесс преобразования не отнимет много времени, т.к. база приложения, обеспеченная компонентами WebBroker, при конвертировании не затрагивается.

Отметим особенности использования той или иной технологии. Технология CGI позволяет обновлять файлы скриптов либо исполняемых модулей без перезапуска HTTP-сервера. Достаточно удалить старый файл и скопировать на его место обновленный файл. CGI - самый дешевый способ разработки Web-приложений. Многие бесплатные программные среды позволяют создавать консольные CGI-приложения. Технологии ISAPI/NSAPI и Apache Shared Module отличаются тем, что ISAPI/NSAPI работает с ISS, а Apache Shared Module работает с Apache. И то и другое - динамические DLL-библиотеки (для Apache обычно используется расширение *.SO). Apache грузит все модули при своем запуске и выгружает их при своем завершении. Для обновления модуля требуется остановить работу демона Apache, удалить старый файл модуля и скопировать на его место новый файл. ISS грузит DLL-модули только при обращении к ним (из браузера), а выгружает их при своем завершении, однако в окне свойств сайта IIS есть кнопка "Выгрузить", позволяющая выгрузить DLL-модуль без завершения работы службы IIS. При использовании DLL (SO) - модулей очень легко можно нарушить работоспособность HTTP-сервера, т.к. модуль работает в адресном пространстве сервера (однако если HTTP-сервер создает дополнительные процессы для обработки клиентских запросов, то нарушить его работоспособность будет куда сложнее). При использовании CGI ваше приложение и HTTP-сервер работают в разных процессах, поэтому ошибки вашего приложения не влияют на работоспособность HTTP-сервера. При каждом клиентском запросе HTTP-сервер запускает CGI-приложение, и в качестве параметров передает данные клиентского запроса. После обработки запроса CGI-приложение отдает результаты обратно HTTP-серверу и завершает свою работу (это относится и к PHP и к Perl). В один и тот же момент времени одно и то же CGI-приложение может обрабатывать несколько клиентских запросов. Это означает, что HTTP-сервер запустит несколько копий CGI-приложения (столько, сколько нужно). Это нужно учитывать, если CGI-приложение использует глобальные ресурсы (например файлы). Доступ к таким ресурсам следует синхронизировать (например, с помощью мьютексов) (при использовании скриптов такой проблемы нет, т.к. интерпретатор сам выполняет необходимую синхронизацию). Если Web-приложение реализовано в виде DLL (SO) - модуля (по технологии WebBroker), то при обработке клиентского запроса ядро WebBroker создает объект TWebModule. Этот объект отвечает за обработку запросов и выдачу результата. После окончания обработки запроса DLL-библиотека не выгружается, а WebBroker не спешит удалять созданный ранее объект TWebModule. Тот же самый объект будет использован при обработке следующих запросов (это делается в целях улучшения производительности Web-приложения, при этом уменьшается время отклика на клиентские запросы). Это нужно иметь ввиду. Нужно перед обработкой каждого запроса выполнять инициализацию глобальных объектов (переменных), и объектов (переменных), которые являются полями объекта TWebModule ("выполнять" естественно при необходимости). Кроме того, что объекты TWebModule не удаляются после обработки запроса, они при каждом запросе могут работать в разных потоках (threads). Это тоже нужно иметь ввиду. Если ваше Web-приложение использует функциональность какого-либо COM-сервера (или DCOM-сервера), то вы перед началом работы с COM/DCOM осуществляете вызов ActiveX.CoInitialize(nil), а по окончании работы вызываете ActiveX.CoUninitialize, причем эти функции привязаны к тому потоку, из которого они вызываются. Для корректной работы COM/DCOM может потребоваться каждый раз при обработке клиентских запросов осуществлять вызов ActiveX.CoInitialize(nil), создать необходимые COM-объекты, а после окончания работы с COM/DCOM нужно будет удалить все созданные COM-объекты и вызвать функцию ActiveX.CoUninitialize. Можно поступить иначе - создать отдельный поток, который будет работать на протяжении всего времени работы Web-приложения, и всю работу с COM/DCOM выполнять именно в этом потоке (правда это может оказаться более опасным, т.к. при неумелой работе с дополнительными потоками вы рискуете повесить не только ваше Web-приложение, но и весь HTTP-сервер).

Создание CGI-приложения по технологии WebBroker

Запустите Delphi и выберите меню File / New / Other / Web Server Application. Выберите модель Web-приложения "CGI stand-alone executable" и нажмите "ОК". Сохраните все файлы проекта. Основной файл проекта назовите cgitest2.dpr, а pas-файл назовите "webunit.pas". Разработаем простое Web-приложение, состоящее из 2-х страниц: главная страница и страница регистрации. По умолчанию будет загружаться главная страница. Она будет содержать приветствие и ссылку на страницу регистрации. На странице регистрации пользователь может ввести свое имя и нажать кнопку "Принять", при этом будет загружена страница приветствия, на которой пользователь увидит свое имя. Для хранения имени пользователя будем использовать кукисы (имя зачастую только для таких целей и пользуются, а остальную информацию о пользователе хранят уже на сервере).
unit WebUnit;

interface

uses
  SysUtils, Classes, HTTPApp;

type
  TWebModule1 = class(TWebModule)
    procedure WebModule1acDefaultAction(Sender: TObject;
      Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
    procedure WebModule1acMainAction(Sender: TObject; Request: TWebRequest;
      Response: TWebResponse; var Handled: Boolean);
    procedure WebModule1acRegFormAction(Sender: TObject;
      Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  WebModule1: TWebModule1;

implementation

{$R *.dfm}

{Обработчик неопознанных страниц}
procedure TWebModule1.WebModule1acDefaultAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
begin
  // Если пользователь указал в адресной строке только
  // http://ServerName/webtest/cgitest2.cgi, то выводим главную страницу.
  // В остальных случаях выводим сообщение "Запрашиваемая страница не найдена"
  if (Request.PathInfo = '') or (Request.PathInfo = '/') then
    WebModule1acMainAction(Sender, Request, Response, Handled)
  else
  begin
    Response.Content :=
      '<h1>Приложение WebBroker</h1><br>' + #13#10 +
      '<h2>Запрашиваемая страница не найдена</h2><br>' + #13#10 +
      '<h2>Перейдите на <a href="main">главную страницу</a></h2>' + #13#10 +
      '<p><center>(c) My Company, 2009</center></p>';
  end;
end;

{Обработчик страницы "main"}
procedure TWebModule1.WebModule1acMainAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  Page, AList: TStringList;
  SUserName: string;
begin
  Page := TStringList.Create;
  AList := TStringList.Create;
  try
    // Если пользователь перешел на эту страницу со страницы регистрации, где нажал
    // кнопку "Принять", то записываем имя пользователя в кукисы в переменную "c_usr"

    // Анализируем POST-запрос клиента. Имя пользователя должно быть с переменной UserName
    if Request.ContentFields.Values['UserName'] <> '' then
    begin
      // Добавляем имя пользователя в список AList
      SUserName := Request.ContentFields.Values['UserName']; // Запомнили имя пользователя
      AList.Values['c_usr'] := SUserName;

      // Устанавливаем кукисы. Они будут храниться на компьютере клиента 100 дней.
      Response.SetCookieField(AList, '', '', Now + 100, False);
    end;        

    Page.Add('<h1>Приложение WebBroker</h1><br>');

    // Если переменная SUserName уже имеет значения, то выводим его, иначе
    // анализируем кукисы из клиентского запроса
    if SUserName = '' then
    begin
      AList.Clear; // Очищаем список
      Request.ExtractCookieFields(AList); // Извлекаем кукисы из клиентского запроса

      // Если в списке кукисов присутствует поле с именем пользователя, то
      // присваиваем его переменной SUserName
      if AList.Values['c_usr'] <> '' then
        SUserName := AList.Values['c_usr'];
    end;

    // Формируем строку приветствия
    if SUserName = '' then
      Page.Add('<p>Добро пожаловать, неизвестный пользователь!</p><br>')
    else
      Page.Add('<p>Добро пожаловать, ' + SUserName + '!</p><br>');

    // Размещаем ссылку на форму регистрации
    Page.Add('<p>Вы еще не зарегистрированы? '+
             'Тогда вам <a href="regform">сюда</a></p><br>');
    Page.Add('<p><center>(c) My Company, 2009</center></p>');

    Response.Content := Page.Text;
  finally
    Page.Free;
    AList.Free;
  end;
end;

{Обработчик страницы "regform"}
procedure TWebModule1.WebModule1acRegFormAction(Sender: TObject;
  Request: TWebRequest; Response: TWebResponse; var Handled: Boolean);
var
  Page, AList: TStringList;
  SUserName: string;
begin
  Page := TStringList.Create;
  AList := TStringList.Create;
  try
    // Извлекаем имя пользователя из кукисов.
    // Это необязательно, и делается лишь для того, чтобы поле ввода имени
    // пользователя заполнялось автоматически
    begin
      Request.ExtractCookieFields(AList); // Извлекаем кукисы из клиентского запроса

      // Если в списке кукисов присутствует поле с именем пользователя, то
      // присваиваем его переменной SUserName
      if AList.Values['c_usr'] <> '' then
        SUserName := AList.Values['c_usr'];
    end;

    Page.Add('<h1>Приложение WebBroker</h1><br>');
    Page.Add('<h2>Регистрация пользователя</h2><br>');

    // Начало формы ввода данных. Будем использовать метод передачи "POST"
    Page.Add('<form action="main" method="post"><br>');

    // Поле ввода данных "text" с именем "username" и значением по умолчанию SUserName
    Page.Add(' Введите ваше имя: <input type="text" '+
             'name="username" value="' + SUserName + '"><br>');

    // Кнопка "Принять" с подсказкой "Сохранить имя"
    Page.Add('<input type="submit" value="Принять" title="Сохранить имя"><br>');

    Page.Add('</form>');
    // Ворма ввода данных закончена

    // Устанавливаем ссылку на главную форму.
    Page.Add('<p>Вы здесь оказались по ошибке? '+
             'Перейдите на <a href="main">главную страницу</a></p>');

    Page.Add('<p><center>(c) My Company, 2009</center></p>');

    Response.Content := Page.Text;
  finally
    Page.Free;
    AList.Free;
  end;  
end;

end.

В данной программе создано 3 действия (actions) с названиями acDefault, acMain, acRegForm. Действия соответствуют страницам, которые запрашивает пользователь. Каждое действие имеет обработчик OnAction, который вызывается в нужный момент ядром WebBroker. Если пользователь ввел адрес "http://localhost/webtest/cgitest2.cgi/main", то WebBroker вызовет обработчик действия acMain: WebModule1acMainAction. Для этого WebBroker выполнит разбор строки запроса, извлечет из нее имя страницы "/main", затем в цикле выполнит перебор всех действий, пока не найдет действие, у которого PathInfo = "/main". Именно его обработчик OnAction и будет вызван. Для того, чтобы добавить новое действие, следует дважды щелкнуть на Web-модуле, в результате чего появится окно со списком всех действий, в котором нужно добавить необходимые действия. Ниже представлено содержимое моего Web-модуля в текстовом виде:

object WebModule1: TWebModule1
  OldCreateOrder = False
  Actions = <
    item
      Default = True
      Name = 'acDefault'
      OnAction = WebModule1acDefaultAction
    end
    item
      Name = 'acMain'
      PathInfo = '/main'
      OnAction = WebModule1acMainAction
    end
    item
      Name = 'acRegForm'
      PathInfo = '/regform'
      OnAction = WebModule1acRegFormAction
    end>
  Left = 149
  Top = 82
  Height = 150
  Width = 215
end

Также, как и в предыдущем примере, настройте в опциях проекта параметр Output directory (установите "c:\inetpub\webtestdir\"), и измените расширение с помощью команды {$E cgi}. Выполните компиляцию проекта. Убедитесь, что сайт работает (для этого введите в браузере адрес "http://localhost/webtest/cgitest2.cgi/"). Сперва вы окажетесь на главной странице, и увидите приветствие "Добро пожаловать, неизвестный пользователь!". Щелкните на ссылке, которая находится справа от предложения "Вы еще не зарегистрированы?", и попадете на страницу регистрации. Введите свое имя и нажмите "Принять". В результате вы вновь попадете на главную страницу, и увидете вместо "неизвестный пользователь" свое имя. Попробуйте прямо в адресной строке вручную указать "левую" страницу, например "main1". Вы окажетесь на странице с текстом "Запрашиваемая страница не найдена". Таким образом сделано все, что было нами запланировано.

Установка CGI-приложения на сервер Apache

В данный момент у вас запущен HTTP-сервер IIS. Перед запуском Apache следует остановить данную службу, иначе Apache не сможет прослушивать стандартный порт 80, т.к. этот порт уже занят службой IIS. Для останова IIS нужно щелкнуть правой кнопкой мыши на иконке "Мой компьютер", выбрать "Управление", развернуть узел "Службы и приложения", щелкнуть на пункте "Службы", найти службу "Веб-публикации", щелкнуть на ней правой кнопкой мыши и выбрать "Стоп". При необходимости вы сможете запустить ISS, для этого нужно будет запустить службу "Веб-публикации".

Вот теперь можно запустить Apache. Если у вас он не установлен, то установите. У меня установлен Apache 1.3, но и с более новыми версиями, думаю, сложностей не будет. Теперь запустите Apache. Его можно запустить либо как службу, либо как консольное приложение. Последний вариант более предпочтителен, т.к. в окно консоли выводятся все ошибки, возникшие при работе Apache. Останов Apache также проще (быстрее) производить в окне консоли (Ctrl+C). После того, как Apache был запущен, скопируйте файл "c:\Inetpub\webtestdir\cgitest2.cgi" в каталог cgi-bin\. У меня это: "c:\Program Files\Apache Group\Apache\cgi-bin\". Введите в браузере адрес "http://localhost/cgi-bin/cgitest2.cgi/". Должна открыться главная страница сайта. Выполните регистрацию (она требуется повторно, поскольку адрес сайта теперь другой), и убедитесь, что ваше имя красуется на главной странице сайта. И на IIS и на Apache сайт должен работать одинаково. При желании вы можете сделать так, чтобы адрес сайта для Apache совпадал с адресом сайта для IIS. Это потребует небольшой корректировки в файле конфигурации Apache "httpd.conf".

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

Разработка ISAPI-приложения для Internet Information Services

Материал из раздела Модели Web-приложений и их особенности дает общее представление о технологии ISAPI/NSAPI. В данном разделе мы выполним конвертацию проекта cgitest2.dpr из CGI в ISAPI/NSAPI. В результате будет создан DLL-модуль, который будет выполнять те же задачи, что и cgitest2.cgi. Запустите Delphi и выберите меню File / New / Other / Web Server Application. Выберите модель Web-приложения "ISAPI/NSAPI Dynamic Link Library" и нажмите "ОК". Сохраните все файлы проекта в новом каталоге. Основной файл проекта назовите isapitest.dpr, а pas-файл назовите "webunit.pas". Теперь из каталога с файлами проекта cgitest2.dpr скопируйте файлы webunit.pas и webunit.dfm в каталог с проектом isapitest.dpr. Файловый менеджер запросит подтверждение на замену файла (нажать "ОК"). Укажите в опциях проекта (Project / Options / Directories/Conditionals / Output directory) каталог, в который будет размещен файл "isapitest.dll". Укажите каталог "c:\inetpub\webtestdir\". Выполните компиляцию проекта и убедитесь, что файл "c:\inetpub\webtestdir\isapitest.dll" присутствует. Запустите службу IIS (перед этим остановите HTTP-сервер Apache, если он запущен). Убедитесь, что сайт работает (для этого введите в браузере адрес "http://localhost/webtest/isapitest.dll/"). Логика работы сайта должна быть абсолютно такой же, как и предыдущих примерах. Как видите, процесс преобразования приложения оказался очень простым. Естественно, нужно учитывать, что если раньше мы работали с автономным CGI-приложением (фактически EXE, только с измененным расширением), то теперь имеем дело с DLL-библиотекой, работающей в адресном пространстве приложения "c:\WINDOWS\system32\inetsrv\inetinfo.exe", и ошибки, допущенные разработчиком в DLL-библиотеке могут привести к ошибкам в работе службы IIS. Сложнее стала ситуация с обновлением DLL-модулей. Предварительно нужно выгрузить DLL-модуль с помощью кнопки "Выгрузить" из окна настроек виртуального каталога IIS. Не стоит думать, что эта же DLL будет работать с HTTP-сервером Apache. У Apache свой интерфейс взаимодействия с DLL (SO), а также требуется внести дополнительные настройки в файл конфигурации httpd.conf.

Разработка Apache Shared Module

Данная модель приложения эквивалентна ISAPI/NSAPI, но работает не с IIS, а с HTTP-сервером Apache. В данном разделе мы выполним конвертацию проекта cgitest2.dpr из CGI в Apache Shared Module. В результате будет создан DLL-модуль, который будет выполнять те же задачи, что и cgitest2.cgi. Запустите Delphi и выберите меню File / New / Other / Web Server Application. Выберите модель Web-приложения "Apache 1.x Shared Module" (наличие данного пункта гарантированно имеется в Delphi 7, однако в более поздних версиях пункт может быть убран) и нажмите "ОК". Сохраните все файлы проекта в новом каталоге. Основной файл проекта назовите apsmtest.dpr, а pas-файл назовате "webunit.pas". Теперь из каталога с файлами проекта cgitest2.dpr скопируйте файлы webunit.pas и webunit.dfm в каталог с проектом apsmtest.dpr. Файловый менеджер запросит подтверждение на замену файла (нажать "ОК"). Укажите в опциях проекта (Project / Options / Directories/Conditionals / Output directory) каталог, в который будет размещен файл "apsmtest.dll". Укажите каталог "c:\Program Files\Apache Group\Apache\modules\". Откройте файл проекта (Project / View Source). Там расположен код:

  exports
    apache_module name 'Project1_module';

Измените "Project1_module" на "apsmtest_module";

Добавьте после строки Application.Initialize следующий код:

  ContentType := 'apsmtest-handler';

Выполните компиляцию проекта и убедитесь, что файл "c:\Program Files\Apache Group\Apache\modules\apsmtest.dll" присутствует. Внесем изменения в конфигурационный файл сервера Apache. Откроем с помощью блокнота файл "c:\Program Files\Apache Group\Apache\conf\httpd.conf", найдем раздел "Dynamic Shared Object (DSO) Support" и добавим в конец раздела (перед "Section 2: 'Main' server configuration") следующее описание:

LoadModule apsmtest_module modules/apsmtest.dll
<Location /apachetest>
  SetHandler apsmtest-handler
</Location>

Здесь apsmtest_module (после LoadModule) один в один должен соответствовать тому, что мы задали в директиве exports (в файле apsmtest.dpr). modules/apsmtest.dll определяет имя библиотеки, которая будет загружаться при запуске сервера Apache. Пути можно задавать относительно конфигурационного параметра ServerRoot. С помощью директивы <Location> мы определяем, что Apache будет обращаться к библиотеке "apsmtest.dll" при указании пользователем http-адреса: "http://ServerName/apachetest/". Т.е. пользователь вообще знать не будет, что существует какая-то библиотека "apsmtest.dll". Он просто вводит псевдоним "/apachetest/" и получает желаемый результат. Значение параметра SetHandler должно совпадать со значением переменной ContentType, которое мы задали в файле apsmtest.dpr. Можно было не указывать значение переменной ContentType. В этом случае в SetHandler нельзя будет ничего другого задавать, кроме apsmtest-handler (apsmtest - это имя библиотеки без расширения).

Запустите HTTP-сервер Apache (перед этим остановите службу IIS, если она запущена). Убедитесь, что сайт работает (для этого введите в браузере адрес "http://localhost/apachetest/"). Логика работы сайта должна быть абсолютно такой же, как и предыдущих примерах.

Процесс преобразования Web-приложения из CGI в Apache Shared Module оказался немного сложнее, чем в случае с ISAPI/NSAPI. Однако код приложения практически не изменился. Был немного изменен файл apsmtest.dpr. Кое что пришлось добавить в файл httpd.conf. Но это, согласитесь, копейки. Благодаря WebBroker нам не нужно тратить время на преобразование Web-приложения из одной модели в другую.

Разработка CGI-приложения для публикации базы данных

Мы разработаем простейшее CGI-приложение для публикации базы данных. Сайт будет состоять из двух страниц. На первой странице "main" будут отображены записи из таблицы "country.db" с кнопками "Добавить", "Изменить", "Удалить". На второй странице "editform" будет расположена форма для редактирования полей указанной либо добавляемой записи. Если пользователь хочет удалить выбранную запись, то программа должна запросить у него подтверждение. Физически на странице "editform" модификация базы данных выполняется не будет. Эта форма служит для сбора всех необходимых данных. На ней будут расположены кнопки "ОК" и "Отмена". Когда пользователь нажмет на любую из этих кнопок, будет вновь открыта страница "main", но процедура формирования страницы "main" предварительно внесет необходимые изменения, если пользователь на странице "editform" нажал "ОК".

Код Web-модуля разработанной программы смотрите здесь.

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

Я установил CGI-приложение в каталог /cgi-bin/ сервера Apache. Для входа на сайт использую адрес "http://localhost/cgi-bin/cgidb.cgi/main". Для добавления записи требуется нажать "Добавить", затем в форме ввода указать значения полей и нажать "ОК". Для изменения записи следует выбрать запись (использовать radiobutton в правой колонке), нажать кнопку "Изменить", указать значения полей и нажать "ОК". Для удаления записи следует выбрать запись, нажать "Удалить" и нажать "ОК" в ответ на запрос подтверждения удаления записи. Все функционирует согласно требованиям к задаче!

Приложение должно точно также работать и под IIS (если конечно вы не забыли выполнить настройки анонимного доступа согласно инструкции). Убедитесь, что все работает как следует.

Модуль извлечения файлов из POST-запроса

Вы можете в HTML-тэг <form> добавить директиву enctype="multipart/form-data", а внутри формы добавить элемент input с типом "file". Например:

  Page.Add('<h1>Загрузка файла</h1>');
  Page.Add('<form action="loadfile" method=POST enctype="multipart/form-data">');
  Page.Add('Выберите файл: <input name="afile" type="file">');
  Page.Add('Описание к файлу: <input name="afiletext" type="text">');  
  Page.Add('<input name="btn1" type="submit" value="Загрузить">');
  Page.Add('</form>');

Данная HTML-форма позволяет отправить на сервер любой файл. В одной форме вы можете отправить на сервер сразу несколько файлов и значений элементов ввода (в данном примере на сервер будет отправлен выбранный пользователем файл и текстовое описание к нему).

Максимальный размер файла (точнее - длина POST-запроса), передаваемого на сервер, определается опциями запущенного HTTP-сервера. Если длина POST-запроса не ограничивается, то пользователь можеть отправить на ваш сайт слишком большой файл, и, обрабатывая такой файл, ваше приложение зависнет, либо завершит работу с ошибкой.

Библиотека WebBroker не предоставляет средств для разбора POST-запросов типа "multipart/form-data". Для разбора таких запросов используйте модуль MsMultipartParser. Пример работы с модулем:

   with TMsMultipartFormParser.Create do                                     
   try                                                                       
     Parse(Request);                                                         
     for I := 0 to Files.Count - 1 do                                        
       Files[I].SaveToFile('c:\temp\' + ExtractFileName(Files[I].FileName)); 
   finally                                                                   
     Free;                                                                   
   end;                                                                      

Алгоритм передачи файла браузеру

Мы рассмотрели процесс передачи файла от браузера к серверу. При этом в одном-единственном POST-запросе можно передать как один, так и множество файлов.

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

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

  • При нажатии ссылки "Загрузить файл" браузер передает на сервер GET-запрос и при этом браузер заведомо не может знать то, какой ответ ему вернется: это может быть HTML-страница, изображение, PDF-документ, файл MP3 и т.п. Более того, о содержимом ответа заранее не знает ни HTTP-сервер, ни Web-приложение, которое подготавливает этот ответ.
  • При получении одного GET-запроса сервер может вернуть браузеру только один ответ. При скачивании файлов это означает, что сервер за один раз НЕ МОЖЕТ передать браузеру сразу несколько файлов.
  • Если в ссылке "Загрузить файл" указан статический адрес файла (например http://example.com/files/file.zip), то это, как правило (но не всегда), означает, что за передачу файла браузеру отвечает HTTP-сервер, и никакие дополнительные Web-приложения (CGI, PHP, Perl и т.п.) в этом процессе не участвуют. При этом, как правило, возможна докачка файла (все популярные HTTP-сервера (IIS, Apache и т.п.) поддерживают докачку файла).
  • Если в ссылке "Загрузить файл" указан "динамический" адрес файла (например "http://example.com/download.cgi/file.zip" или "http://example.com/download.cgi?target=file.zip" или "http://example.com/download.cgi?filenum=100" и т.п.), то за передачу файла браузеру отвечает, как правило, Web-приложение (в данном случае: download.cgi). При этом чаще всего докачка файла не поддерживается (реализация докачки требует соответствующих навыков и знаний у разработчика Web-приложений, и эти знания приходят со временем).
  • При получении файла браузер не перезагружает открытую HTML-страницу.

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

procedure TWebModule1.WebModule1actDownloadAction(
  Sender: TObject; Request: TWebRequest;
  Response: TWebResponse; var Handled: Boolean);
var
  ms: TMemoryStream;
  AFileName, AFullFileName: string;
  S: AnsiString;
begin
  AFileName := Request.QueryFields.Values['file'];
  if AFileName = '' then
  begin
    // Сообщаем, что имя файла не задано
    Response.Content := '<h1>Необходимо указать имя файла '+
                        '(после параметра "file")</h1>';
  end else
  begin
    // Предполагаем, что запрашиваемые файлы
    // хранятся в каталоге c:\temp\
    AFullFileName := 'c:\temp\' + AFileName;

    if FileExists(AFullFileName) then
    begin
      ms := TMemoryStream.Create;
      try
        // Загружаем файл в оперативную память
        // (подразумевается, что файл не очень большой)
        ms.LoadFromFile(AFullFileName);

        // Копируем содержимое файла в строку S
        SetString(S, PAnsiChar(ms.Memory), ms.Size);

        // 1. Указываем тип HTTP-ответа		
        Response.ContentType := 'application/octet-stream';		
		
        // 2. Формируем тело HTTP-ответа
        Response.Content := S;

        // Указывает имя файла. Именно это имя браузер будет
        // использовать при сохранении файла на диск
        // Имя файла необходимо указывать ЛАТИНСКИМИ буквами.
        // Русские буквы - НЕДОПУСТИМЫ. Их предварительно
        // следует транслитерировать.
        Response.SetCustomHeader('Content-Disposition',
          Format('attachment; filename=%s', [AFileName]));
      finally
        ms.Free;
      end;
    end else
    begin
      // Сообщаем, что файл с таким именем не найден
      Response.Content := '<h1>Файл "' + AFullFileName +
                          '" не найден!</h1>';
    end;
  end;
end;

Пользователь вводит в адресной строке браузера запрос "http://localhost/webtest/cgitest.cgi/download?file=ИмяФайла" и в результате сервер передает браузеру запрошенный файл, после чего браузер спрашивает у пользователя, что с этим файлом делать дальше (сохранить или открыть).

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

Response.ContentType := 'application/octet-stream';

Для того, чтобы указать браузеру имя файла, используется дополнительная строка HTTP-заголовока "Content-Disposition":

Response.SetCustomHeader('Content-Disposition',
  Format('attachment; filename=%s', [AFileName]));

Имя файла указывайте ТОЛЬКО латинскими буквами и БЕЗ ПРОБЕЛОВ. При необходимости выполняйте транслитерацию с русского на английский. Такое некрасивое ограничение связано с тем, что каждый браузер трактует кодировку поля filename по своему (кто в 1-байтовой кодировке ANSI, а кто в многобайтовой UTF-8). Поэтому, если Вы укажете имя файла русскими буквами в кодировке ANSI Win-1251, то Internet Explorer его отобразит правильно, а FireFox отобразит пустую строку. Если же Вы укажите русское имя файла в кодировке UTF-8 (например переконвертируете с помощью функции AnsiToUtf8()), то FireFox его отобразит правильно, а Internet Explorer покажет кракозябры. И врядли в ближайшем будущем такую проблему получится красиво решить.

Теперь несколько слов о докачке. Перед скачиванием файла браузер еще не знает тип и размер ответа. При получении и начале скачивания файла, его размер и тип становятся известны. Если в процессе скачивания файла пользователь нажмет на "паузу", а затем "возобновить", то браузер запросит у сервера лишь необходимый участок файла, т.е. те байты, которые еще не были скачаны. При этом в заголовок HTTP добавляется параметр Range. Пример такого параметра:
Range: bytes=672906-

Большинство Web-приложений в Интернете не поддерживают докачку, поэтому на такой запрос обычно они возвращают целиком весь файл, при этом браузер выдаст сообщение об ошибке.

Для правильной обработки GET-запросов, которые содержат параметр Range, Web-приложение должно проанализировать диапазон байтов и скопировать в тело ответа только байты из указанного диапазона. Если в заголовке GET-запроса указано только начало диапазона, а затем дефис, то следует вернуть участок до конца файла.

Значение параметра Range в WebBroker извлекается по-разному в зависимости от типа приложения. Для CGI (EXE) используется следующий код:
S := GetEnvironmentVariable('HTTP_RANGE');
Для ISAPI (DLL) используется следующий код:
S := Request.GetFieldByName('Range');

Дальнейший анализ строки и копирование требуемого участка файла в поле Response.Content не составляют особого труда.

Сжатие HTML-страницы для экономии траффика

Старайтесь ВСЕГДА сжимать подготовленные HTML-страницы перед передачей их пользователю!

Эффект от сжатия очень большой. При сжатии экономятся ресурсы сервера (сжатие отнимает ресурсов меньше, чем организация передачи клиенту большого объема текста), экономится траффик (причем как со стороны сервера, так и со стороны пользователя), существенно увеличивается скорость загрузки и отрисовки страницы в браузере (чаще всего браузер сначала загружает полностью весь сжатый HTTP-ответ, а затем в один миг осуществляет отрисовку всей HTML-страницы). Чем больше объем текстовой информации, тем больше степень сжатия. Например, HTML-страница размером 7МБ может быть сжата в 70КБ (разница - в 100 раз). Это реальный пример, но у Вас степень сжатия может отличаться в зависимости от содержимого HTML-страницы.

Сайтов с gzip сжатием в Интернете в сотни раз меньше, чем сайтов без сжатия. Зачастую Web-разработчики и не подозревают о существовании gzip-сжатия (не говоря об обычных пользователях). Попробуйте открыть с помощью Вашего браузера любую HTML-страницу. Как Вы определите, применялось ли к ней gzip-сжатие или нет? Практически никак!

А знаю 2 способа это проверить: 1 - установить браузер FireFox и дополнение к нему FireBug; 2 - установить Fiddler. О наличии gzip-сжатия свидетельствует строка Content-Encoding: gzip в заголовке HTTP-ответа.

Браузер может поддерживать gzip-сжатие, а может и не поддерживать. Если поддерживает, то в заголовке GET-запроса передается примерно такая строка:
Accept-Encoding: gzip,deflate
Вам не следует обращать внимание на deflate. Это устаревший тип сжатия. Современные браузеры поддерживают deflate-сжатие лишь для совместимости со старыми HTTP-серверами. Врядли Вы сейчас найдете браузер, который поддерживает deflate, но при этом не поддерживает gzip.

Используйте gzip-сжатие только для сжатия текстовой информации. Файлы PDF, PNG, GIF, JPEG, PDF, инсталляторы EXE и т.д. обычно уже сжаты, и поэтому их повторное сжатие не целесообразно и попусту растрачивает ресурсы сервера.

В WebBroker действия по сжатию следует осуществлять в конце обработчика WebModuleAfterDispatch. Ниже приведен пример gzip-сжатия:

procedure TAZSWebModule.WebModuleAfterDispatch(
  Sender: TObject; Request: TWebRequest; 
  Response: TWebResponse; var Handled: Boolean);
var
  SAcceptEncoding, STmp: string;
begin          
{$IFDEF WEBCGI} // Для CGI
   SAcceptEncoding := GetEnvironmentVariable('HTTP_ACCEPT_ENCODING');
{$ELSE} // Для ISAPI
   SAcceptEncoding := Request.GetFieldByName('Accept-Encoding');
{$ENDIF}

  // Если подготовлена HTML-страница
  if Pos('text/html', LowerCase(Response.ContentType)) > 0 then
  begin   
    if Pos('gzip', LowerCase(SAcceptEncoding)) > 0 then
    begin
      STmp := ZCompressStrG(Response.Content, '', '', 0);
      Response.Content := STmp;
      Response.ContentEncoding := 'gzip';
    end else
    if (Pos('deflate', LowerCase(SAcceptEncoding)) > 0) and 
	  (Pos('MSIE 6.0', UpperCase(Request.UserAgent)) = 0) then
    begin
      // для MSIE 6.0 сжатие deflate не делаем
      STmp := ZCompressStr(Response.Content);
      Response.Content := STmp;
      Response.ContentEncoding := 'deflate';
    end;
  end;
end;

На всякий случай здесь приводится также пример deflate-сжатия. Deflate-сжатие для IE6 не делается, т.к. этот браузер не может правильно распаковать данные, сжатые методом deflate, и вместо HTML-страницы отображает пустую страницу.

ZCompressStrG и ZCompressStr - это функции из модуля ZLIBEX.

Кстати Вы сейчас читаете страницу более 60КБ без gzip-сжатия :) Как только будет время - обязательно добавлю!

Выбор кодировки: WIN-1251 vs. UTF-8

WIN-1251 - это российская кодовая страница однобайтовой кодировки ANSI, в которой первые 127 символов - стандартные (латиница), а последние 128 символов - кириллица. Т.е. Ваша HTML-страница может содержать либо латинские, либо русские буквы. Вы не сможете выводить текст на других языках (ни на немецком, ни на французском, ни даже на украинском, ну а тем более на китайском).

UTF-8 - это многобайтовая кодировка, в которую включены все мировые языки. Определение "многобайтовая" означает, что разные языки могут кодироваться разным числом байтов: латиница - 1-м байтом, кириллица 2-мя байтами, различные иероглифы - 4-мя байтами. Кодировка UTF-8 совместима с Unicode, однако в Unicode любой символ кодируется фиксированным числом байтов (например, 2 байта). В принципе, с точки зрения американцев, UTF-8 и ANSI - это одно и то же. С точки зрения остальных - это совершенно разные кодировки. Используя кодировку UTF-8 Вы можете одновременно использовать на своей HTML-странице любые символы из любых языков.

Кодировка UTF-8 уже давно является стандартом для Интернета, поэтому делайте выбор в пользу UTF-8 не раздумывая!

WebBroker в Delphi 2010 имеет встроенную поддержку UTF-8. Для ее включения достаточно указать в обработчике WebModuleBeforeDispatch следующую строку:
Response.ContentType := 'TEXT/HTML; CHARSET=UTF-8';

В старых версиях Delphi этого недостаточно. Требуется также добавить в обработчик WebModuleAfterDispatch следующий код:

procedure TWebModule1.WebModuleAfterDispatch(
  Sender: TObject; Request: TWebRequest;
  Response: TWebResponse; var Handled: Boolean);
begin
  {$IF RTLVersion < 20.00}
  if Pos('TEXT/HTML; CHARSET=UTF-8',
    AnsiUpperCase(Response.ContentType)) > 0 then
    Response.Content := AnsiToUtf8(Response.Content);
  {$IFEND}
end;

Инструментарий Web-разработчика из разряда "must have" (должен иметь)

Опишу лишь самую малость. Во-первых, эффективная Web-разработка на мой взгляд совершенно немыслима без Fiddler. Fiddler позволяет просматривать абсолютно весь HTTP-траффик между вашим браузером (а также любыми другими приложениями) и интернетом. Благодаря Fiddler многие вещи, казавшиеся раньше непонятными и сложными, становятся простыми и очевидными. Возможности встроенного поиска Fiddler позволяют за считанные секунды найти все обращения к любому хосту, а это помогает при выяснении причины заражения той или иной HTML-страницы. Если Ваша HTML-страница заражена (симптомы: куда не ткни - вылазит страница с "нежелательным" содержимым), то Вы за пять минут выясните причину: либо виноват один из счетчиков, установленным Вами на HTML-странице, либо вирус на сервере заразил Ваши скрипты, либо вирус добавил свой HTTP-фильтр на сервере и т.д.

В качестве основного браузера рекомендую FireFox, при этом обязательно должно быть установлено дополнение FireBug. FireBug - это на удивление эффективное и мощное средство для разбора кода HTML-страницы, быстрой настройки стилей, отладки Java Script и многого другого. Кроме того, FireBug осуществляет мониторинг всех HTTP-запросов, связанных с данной страницей (все же для этих целей намного лучше использовать Fiddler).

Кроме того, рекомендую установить дополнение YSlow. Мне оно помогает в тех случаях, когда требуется быстро преобразовать сжатые скрипты Java-Script в читабельный вид. Кроме того, Вы найдете в YSlow еще много полезных возможностей.

Инструменты, выручающие не только при Web-программировании, но и любом программировании, а также при исследовании работы сторонних программ: Process Monitor и Process Explorer.

Заключение

В статье показаны некоторые приемы программирования Web-приложений в Delphi. Приведены сведения по установке и настройке HTTP-серверов ISS и Apache. Уделено особое внимание разработке CGI-приложений, способных работать под управлением любого Web-сервера без необходимости перекомпиляции. Даны сведения по разработке Web-приложений по технологии ISAPI/NSAPI, а также Apache Shared Module.

Ни слова не было сказано о дополнительных компонентах WebBroker: TPageProducer, TDataSetTableProducer, TDataSetPageProducer и др. Я полагаю, читатель самостоятельно разберется со всеми этими компонентами, если будет желание.

Использованные источники

  • Программирование Интернет приложений в Borland Kylix. Часть II. - http://www.delphimaster.ru/articles/...
  • Apache Shared Modules in Delphi - http://www.blong.com/Articles/Apache For Windows
  • Google - http://www.google.ru
  • Help for Delphi 7
  • Пачеко К., Тейксейра С. Delphi 5: Руководство разработчика. В 2-х т. Т. 2: Разработка компонентов и работа с базами данных. – М., 2000.–992 с. (доступно в электронном виде в Internet)
  • Подольский С., Скиба С, Кожедуб О. Разработка интернет-приложений в Delphi . — СПб.: БХВ-Петербург, 2002.–432с - http://www.interface.ru
  • HTML Code Tutorial - http://www.htmlcodetutorial.com/
Логинов Дмитрий © 2005-2015