ГЛАВА 15

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

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

  •  Возможность прямого обмена данными между клиентскими задачами. Использование запуска отдельного экземпляра модуля-обработчика для каждого запроса рождает следующую проблему: как обмениваться данными между экземплярами модулей. Как правило, она решается путем сохранения данных в файл. Этот способ вынуждает предусматривать обработку конфликтов совместного доступа к файлам, когда один экземпляр модуля еще не завершил транзакцию, а второй уже пытается открыть этот общий файл для чтения или записи. При большом количестве одновременных попыток доступа возникает очередь и, как следствие, потеря возможности одновременного (или почти одновременного) обслуживания клиентов. Эта конфликтная ситуация может быть решена путем использования глобальных переменных в одной программе. Поскольку время обработки дисковых операций значительно больше времени работы с памятью, даже если данные находятся в дисковом кэше, то распараллеливание операций (даже не совсем полноценное) становится более простым. Если же используется способ сохранения данных в БД, настроенной на обработку параллельных запросов, то все равно SQL-сервер требует достаточно много времени на проведение транзакции, что может существенно сказаться на быстродействии системы.
  •  Экономия памяти сервера. Действительно, при работе с Web-сервером и CGI-модулями каждый запрос приводит к запуску своего экземпляра модуля, что неэффективно с точки зрения распределения памяти.
  •  Увеличение общего быстродействия системы. Поскольку каждый запускаемый экземпляр модуля требует для загрузки определенного времени, состоящего из чтения ЕХЕ-файла с диска, регистрации процесса в системе и т. д., то единожды запущенная программа — комплексный сервер сильно сэкономит рабочее время процессора сервера и сократит время обработки клиентских запросов. Кроме того, каждый процесс, естественно, требует вычислительных ресурсов, что при большом их количестве приводит к задержкам в обработке клиентских запросов.
  •  Централизованное решение вопросов безопасности. При написании серверных модулей нужно распределять задачи контроля безопасности общей системы между собственно CGI-модулем, Web-сервером, операционной системой, сервером базы данных. На самом деле, отследить все "узкие места", которыми может воспользоваться хакер в каждом компоненте системы — трудная задача. Кроме того, и операционная система, и Web-сервер разрабатываются с позиций универсальности, предполагая решение разнотипных задач, о которых конечный разработчик CGI-модуля может и не подозревать. Эта универсальность является либо мощным фундаментом для выведения всей системы из строя, либо работы с непредусмотренной программистом логикой, что еще хуже. Создавая собственный сервер, программист предусматривает только тот алгоритм, который обеспечивает выполнение- его, и только его задач. При этом, разумеется, разграничение прав доступа к диску и реализация сетевых операций на низком уровне находятся в компетенции операционной системы, но они, как правило, являются достаточно защищенными участками в общей структуре безопасности ОС.
  •  Статичность объектов системы. При создании служб, предполагающих обмен данными между клиентом и сервером, состоящий из нескольких последовательных фаз, трудно применять обычные подходы к проектированию. Каждый раз, когда модуль выполнил определенный, пусть даже очень небольшой, блок задач, сгенерировав ответ клиенту, его работа должна быть завершена. При этом, естественно, все объекты, на базе которых были созданы программы, уничтожаются. Для последующего восстановления состояния их полей (свойств), последние нужно сохранять, например, в скрытых (hidden) элементах Web-страницы, отправленной клиенту. Такая ситуация порою очень неудобна и CGI-программисты прибегают к технологии так называемых "долгоживущих" модулей, один запущенный экземпляр которого может обслуживать несколько последовательных запросов одного клиента. Вместе с тем, программируя сам сервер, можно использовать более гибкие средства как для работы с каждым клиентом, так и для менеджмента всех соединений в комплексе.

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

  •  Каждый пользователь входит в чат со специальной страницы, где он может указать свое имя, отображаемое затем для идентификации принадлежности сообщения.
  •  Контроль пользователей ведется посредством отслеживания их IP-адресов.
  •  Попытка двойного входа в чат должна быть запрещена. Это значит, что каждый клиент имеет определенное, указанное имя, и в случае, когда он производит попытку входа в чат с другим именем, доступ к чату этого клиента с новым именем должен быть заблокирован.
  •  Формирование графического отображения 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. Описание данных, отправленных клиентом 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 указывается количество символов, составляющих розт-запрос. Само тело запроса является последним набором данных, передаваемых от клиента к серверу.
Здесь так много внимания было уделено анализу содержимого заголовков в связи с тем, что их корректная обработка является залогом надежной работы всего сервера.
 
Обзор структуры сервера
Создаваемая программа будет состоять из нескольких функциональных частей:

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

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

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г. Яндекс.Метрика