Web Application Firewall (ModSecurity) в действии

Атаки на уровень web-приложений одни из самых распространенных и часто крайне критичны. В данной статье хочу показать насколько способен WAF ModSecurity отражать данные угрозы.

1. Межсетевой экран уровня web-приложений Modsecurity

1.1 Проект ModSecurity

ModSecurity создан Иваном Ристиком (Ivan Ristic) в 2003 году и представляет собой firewall Web-приложений, который может использоваться как модуль Web-сервера Apache, либо работать в автономном режиме и позволяющий защитить Web-приложения как от известных, так и неизвестных атак. Его использование прозрачно, как установка, так и удаление не требует изменения настройки сервисов и сетевой топологии. Кроме того, при обнаружении уязвимого места теперь не обязательно впопыхах изменять исходный код, делая новые ошибки, достаточно на первых порах добавить новое правило, запрещающее вредную комбинацию. Modsecurity может защищать одновременно несколько Web-серверов, в том числе и отличных от Apache [1].

Кроме обычных соединений может контролировать и защищенный SSL-трафик. Гибкозадаваемые правила журналирования позволяют записать любые данные сеанса, позволяя в будущем полностью разобрать запросы, предшествующие взлому. Понимание протокола HTTP позволяет очень тонко выполнить фильтрацию, поддерживается как GET, так и POST-методы, запросы к динамическим ресурсам. Для борьбы с различными методами уклонения (ссылки на внешние сайты, закодированные URL, использование нескольких слэшей, нулевые байты и пр.) пути и параметры нормализуются. В новой версии, возможно наследование правил, используется новый формат контрольного журнала, позволяющий производить аудит в реальном времени. Кроме того, в этой версии введен новый механизм, названный guardian, задачей которого является контроль запросов, производимых с одного адреса, что позволяет защититься от DOS-атак, настраивая правила iptables. Интеграция с Open Source антивирусом ClamAV дает возможность сканировать загружаемые файлы на наличие вирусов. В версии добавлена поддержка PCRE, которую теперь можно использовать вместо библиотеки regex для Apache. Количество кода по сравнению с предыдущей версией выросло на 40%. Распространяется ModSecurity под двойной лицензией: GNU GPL, как свободное Open Source приложение, также доступно несколько вариантов end-user и OEM коммерческих лицензий [1].

1.2 Варианты установки ModSecurity

Существует два типовых варианта установки ModSecurity [3].
а) Установка непосредственно на Web-сервер.
б) Установка в качестве обратного прокси-сервера (Reverse Proxy).

Надо сказать, что ModSecurity используется только с Web-серверами Apache, желательно версий 2.x. Сервер Apache может работать как под Linux/UNIX, так и под Windows (http://httpd.apache.org). Mодуль ModSecurity изначально создавался под Linux/UNIX (http://www.modsecurity.org), но благодаря работе Стефана Лэнда был успешно портирован под Microsoft Windows (http://www.apachelounge.com). Сравним, чем отличается установка ModSecurity непосредственно на Web-сервер от установки в качестве обратного proxy [3].

В первом случае ModSecurity перехватывает все запросы к Web-серверу, на котором он установлен: запрос от клиента сначала сравнивается с правилами фильтрации модуля ModSecurity, а затем передается на дальнейшую обработку Web-серверу. ModSecurity также может контролировать ответы Web-сервера, т.е. после формирования страницы ответ обрабатывается модулем и в соответствии с правилами блокирует или разрешает прохождение ответа Web-сервера. Последний вариант можно использовать для замены страниц с ошибками Web-приложений, возникающими в результате обработки Web-сервером запросов от клиента, на стандартные страницы (например, страницы с ошибкой 404). Это позволит уменьшить объем передаваемой клиенту критичной информации о работе приложения и усложнит задачу потенциального злоумышленника [3].

Данный вариант установки ограничен применением Apache в качестве Web-сервера и не может использоваться для защиты других Web-серверов.

Установка Apache c модулем ModSecurity в качестве обратного proxy-сервера позволяет перехватывать запросы клиентов к Web-серверам, развернутым на любой платформе (IIS, Apache, WebSphere и т.д.). Принцип работы обратного proxy-сервера заключается в том, что сервер переадресует все запросы от клиентов Web-серверу и при получении ответа перенаправляет его клиенту. Клиенты «видят» только внешний интерфейс proxy-сервера и «не видят» расположенных за ним Web-серверов. В данном случае недостатками являются необходимость закупки дополнительного оборудования под proxy-сервер и наличие единой точки отказа для одного или нескольких Web-серверов. Однако сам proxy-сервер может выполнять и дополнительные полезные функции, например кэширование статических запросов и балансировку нагрузки на группу Web-серверов [3].

1.3 Функционал и преимущества ModSecurity

Вот несколько из основных возможностей, предоставляемых модулем ModSecurity.
а) Анализ запроса. Это основная функция данного модуля, особенно когда вы имеете дело с POST запросами, в которых получение тела запроса может быть затруднено [2].
б) Выполнение канонизации и функции антиуклонения. Выполнение ряда преобразований, для преобразования входных данных в форму, подходящую для анализа. Этот шаг применяется для борьбы с различными методами уклонения.
в) Выполнение специальных встроенных проверок. В этом месте выполняются более сложные проверки правильности, такие как проверка правильности URL кодирования и проверка правильности Unicode кодирования. Вы можете также контролировать некоторые значения байтов в запросе для борьбы с shellcode.
г) Запуск входных правил. В этом месте запускаются правила, созданные вами. Они позволяют вам анализировать каждый аспект запроса, используя регулярные выражения. Также здесь могут быть объединены несколько правил для более сложного анализа [2].
Затем запрос достигает обработчика. После запроса выполняются нижестоящие действия.
а) Запуск правил вывода. Правила вывода применяются к телу ответа. Они очень полезны для предотвращения утечек информации [2].
б) Регистрация запроса. Регистрируется окончательный запрос, состоящий из тела запроса и заголовков ввода и вывода. Чтобы предотвращать чрезмерную регистрацию, ModSecurity может регистрировать запросы по выбору, например запросы, которые получили ответ от ModSecurity [2].

Преимущества ModSecurity заключаются в следующем.
1) Бесплатный.
2) Открытые исходные коды.
3) Фильтрует как исходящие, так и входящие данные.
4) Удобный язык для написания правил фильтрации, базированный на проверке по регулярным выражениям.
5) Интеграция с прокси модулями Apache, что позволяет оптимально ускорить работу Web-сервера (loadbalancing).
6) Графическая оболочка для написания правил безопасности (REMO) [4].

ModSecurity на хабре:
1) Бета-версия modSecurity для Nginx
2) Защита веб-сервера Apache от атаки медленного чтения, а так же некоторых других направленных атак

2. Защита сервера с помощью WAF ModSecurity

На рисунке 1.1 изображена схема практической реализации атакующего воздействия на защищенный ModSecurity сервер с проектом Webgoat (подробнее данный проект рассмотрен в конце статьи). Атакующий с помощью специализированного ПО (браузер MS Internet Explorer, прокси-сервер WebScarab, сканер Acunetix WVS 6) будет атаковать Web-сервер с предустановленным проектом WebGoat. В последующем схема работы будет изменена – после соответствующей настройки Web-сервера c установленным межсетевым экраном прикладного уровня, настроенного в качестве обратного сервера-посредника по отношению к серверу с проектом WebGoat, будем атаковать данный сервер.


Рисунок 1.1

2.1 Руководство по настройке ОС Windows Server 2003 (ModSecurity)

Установим ModSecurity в качестве обратного прокси-сервера по отношению к серверу с проектом WebGoat, для этого:
1. Зайдем в «Пуск-Панель управления-Сетевые подключения». По подключению «Подключение по локальной сети» необходимо нажать правой кнопкой мыши, затем «Свойства». Выбрать «Протокол Интернета(TCP/IP)» и установить ip-адрес 192.168.10.2, маска 255.255.255.0.
2. Рассмотрим универсальный вариант использования ModSecurity в качестве обратного сервера-посредника (Reverse Proxy). Для работы сервера Apache в качестве обратного proxy-сервера необходимо включить поддержку следующих модулей:
1) mod_unique_id (входит в состав Apache);
2) mod_proxy (входит в состав Apache);
3) mod_proxy_http (входит в состав Apache);
4) mod_proxy_balancer (входит в состав Apache);
5) mod_proxy_html (не входит в состав Apache).
Устанавливаем Microsoft Visual C++ 2008 (x86). Для этого запускаем vcredist_x86(2008).exe и следуем указаниям мастера установки. Далее распаковываем файл httpd-2.2.21-win32-x86-ssl.zip в папку C:\Apache2. Данные дистрибутивы можно скачать с соответствующих источников [5, 6].
3. Устанавливаем сервер Apache в качестве службы операционной системы. Для этого в командной строке набираем: c:\Apache2\bin\httpd.exe -k install
4. Запускаем ApacheMonitor (С:\Apache2\bin\ApacheMonitor.exe).
Распакуем архивы с библиотекой libxml2.dll и модулем mod_proxy_html в папку c:\Apache2\modules\ (данные архивные файлы можно скачать с соответствующих источников [6])
5. Настроим Web-сервер Apache на работу в качестве обратного proxy-сервера. Для этого отредактируем файл конфигурации c:\Apache2\conf\httpd.conf в соответсвии с нижеприведенным листингом:

ThreadsPerChild 250
MaxRequestsPerChild 0
ServerRoot «c:/Apache2»
Listen 80
LoadFile C:\apache2\modules\libxml2.dll
LoadModule proxy_module modules/mod_proxy.so
LoadModule proxy_balancer_module modules/mod_proxy_balancer.so
LoadModule proxy_http_module modules/mod_proxy_http.so
LoadModule unique_id_module modules/mod_unique_id.so
LoadModule proxy_html_module modules/mod_proxy_html.so
ProxyRequests Off
ProxyPass / 192.168.10.1/
ProxyPassReverse / 192.168.10.1/
ServerName localhost:80
ErrorLog logs/error_log
LogLevel warn

6. В файле конфигурации нас интересует следующие параметры:
Listen 80 — порт, который будет использоваться Web-службой для приема HTTP-запросов от клиентов;
ProxyRequests Off — отключение режима обычного proxy;
ProxyPass и ProxyPassReverse, предназначенные для перенаправления запросов на другой Web-сервер.
В общем виде директивы ProxyPass и ProxyPassReverse имеют следующий синтаксис:
ProxyPass <относительный адрес на proxy-сервере> <адрес Web-сервера для перенаправления>.

7. Попробуем запустить наш proxy-сервер: открываем ApacheMonitor и нажимаем Start.
В случае если proxy-сервер был запущен успешно, то при обращении к нему по HTTP должна отобразиться Web-страница защищаемого Web-сервера.

После настройки и проверки работоспособности proxy-сервера установим непосредственно ModSecurity. Сначала остановим запущенную службу Apache (через ApacheMonitor). Из архива mod_security-2.6.2-win32.zip распаковываем в папку c:\Apache2\modules\ файл mod_security2.so (данный архив можно скачать с соответствующих источников [6]).

8. Для работы с модулем ModSecurity в конфигурационный файл Apache необходимо внести некоторые изменения, а именно добавить следующую строку: LoadModule security2_module modules/mod_security2.so

9. Разархивируйте архив с последними версиями правил для ModSecurity modsecurity-crs_2.2.2.zip (данный архив можно скачать с соответствующего источника [7]) перепишите всю папку rules из архива в папку c:\Apache2\conf\rules

10. Добавьте конфигурационные файлы правил в конфигурационный файл httpd.conf с помощью следующих команд:
include c:\Apache2\conf\rules\base_rules\*.conf
include c:\Apache2\conf\rules\slr_rules\*.conf
include c:\Apache2\conf\rules\*.conf

11. В папке c:\Apache2\conf\rules\находится конфигурационный файл modsecurity_crs_10_config.conf с базовыми настройками модуля ModSecurity.

Удалите базовые настройки. Укажите список действий по умолчанию, при которых будет производится занесение в журналы аудита каждого правила, для которого выполнено соответствие, и запрос будет прерываться с кодом статуса 404 с помощью команды SecDefaultAction «phase:2,deny,log, status:404»

12. Включите фильтрацию с помощью команды SecRuleEngine On

13. Включите сканирование тела запроса и тела ответа с помощью команды SecRequestBodyAccess On и SecResponseBodyAccess On

14. Скройте идентификатор сервера с помощью команды SecServerSignature «Apache/2.2.4 (Fedora)»

15. Укажите папку для загрузки файлов tmp с помощью команды SecUploadDir /tmp

16. Включите поддержку cookie версии 1 с помощью команды SecCookieFormat 1

17. Укажите параметры типовой конфигурации протоколирования событий:

SecAuditEngine RelevantOnly
SecAuditLogRelevantStatus "^(?:5|4\d[^4])"
SecAuditLogType Concurrent
SecAuditLogParts ABCDEFHZ
SecAuditLogStorageDir logs/data/
SecAuditLog bin/mlogc.exe
SecAuditLog "|bin/mlogc.exe bin/mlogc.conf"

18. Добавьте в конфигурационный файл modsecurity_crs_10_config.conf команды, чтобы в итоге они соответствовали рисунку 1.2.


Рисунок 1.2 – Конфигурационный файл modsecurity_crs_10_config.conf

19. Запустите сервер Apache (с помощью ApacheMonitor). В итоге мы получили рабочий proxy-сервер, позволяющий защитить выбранный Web-сервер от ряда атак.

2.2 Руководство по настройке ОС Windows Server 2003 (WebGoat)

Развернем сервер с проектом WebGoat, для этого:

1. Зайдем в «Пуск-Панель уравления-Сетевые подключения». По подключению «Подключение по локальной сети» необходимо нажать правой кнопкой мыши, затем «Свойства». Выбрать «Протокол Интернета(TCP/IP)» и установить ip-адрес 192.168.10.1, маска 255.255.255.0.

2. Установить Java Runtime Environment (JRE) дистрибутив. В процессе установке оставить все настройки по умолчанию

3. Разархивировать архив WebGoat-OWASP_Standard-5.3_RC1.7z в папку «C:\ WebGoat-5.3_RC1»

4. Открываем конфигурационный файл, находящийся по адресу «С:\WebGoat-5.3_RC1\tomcat\conf\server_80.xml» и редактируем настройки подключения внешних ip-адресов следующим образом (см. рисунок 1.3):
Connector address=«127.0.0.1,192.168.10.1,192.168.10.2, 192.168.10.5»

5. Открыть каталог «С:\WebGoat-5.3_RC1» и запустить файл «webgoat.bat»


Рисунок 1.3 — Редактирование конфигруационного файла server_80.xml

2.3 Генерация атакующего воздействия

Для тестирования работы ModSecurity можно использовать любой из сканеров безопасности, умеющий проверять уязвимости веб-сервисов. Воспользуемся Acunetix Web Vulnerability Scanner.

Acunetix Web Vulnerability Scanner автоматизирует задачу контроля безопасности Web приложений и позволяет выявить уязвимые места в защите Web-сайта до того, как их обнаружит и использует злоумышленник.

Acunetix Web Vulnerability Scanner (WVS) работает следующим образом:

1) Acunetix WVS исследует и формирует структуру сайта, обрабатывая все найденные ссылки и собирая информация обо всех обнаруженных файлах;
2) затем программа тестирует все Web-страницы с элементами для ввода данных, моделируя ввод данных с использованием всех возможных комбинаций и анализируя полученные результаты;
3) обнаружив уязвимость, Acunetix WVS выдает соответствующее предупреждение, которое содержит описание уязвимости и рекомендации по ее устранению;
4) итоговый отчет WVS может быть записан в файл для дальнейшего анализа и сравнения с результатами предыдущих проверок.

Acunetix Web Vulnerability Scanner автоматически обнаруживает следующие уязвимости:

1) Cross site scripting (выполнение вредоносного сценария в браузере пользователя при обращении и в контексте безопасности доверенного сайта);
2) SQL injection (выполнение SQL-запросов из браузера для получения несанкционированного доступа к данным);
3) база данных GHDB (Google hacking database) – перечень типовых запросов, используемых хакерами для получения несанкционированного доступа к Web-приложения и сайтам.
4) выполнение кода;
5) обход каталога;
6) вставка файлов (File inclusion);
7) раскрытие исходного текста сценария;
8) CRLF injection
9) Cross frame scripting;
10) общедоступные резервные копии файлов и папок;
11) файлы и папки, содержащие важную информацию;
12) файлы, которые могут содержать информацию, необходимую для проведения атак (системные логи, журналы трассировки приложений и т.д.);
13) файлы, содержащие списки папок;
14) папка с низким уровнем защиты, позволяющие создавать, модифицировать или удалять файлы.

А также идентифицирует задействованные серверные технологии (WebDAV, FrontPage и т.д.) и разрешение на использование потенциально опасных http-методов (PUT, TRACE, DELETE).

На основной машине, выступающей в роли сканирующей станции, установлен Acunetix Web Vulnerability Scanner. Для тестирования работы межсетевого экрана прикладного уровня вначале протестируем на наличие Web-уязвимостей Web-сервер с установленным проектом WebGoat (с ip-адресом 192.168.10.1), а затем просканируем сервер (с ip-адресом 192.168.10.2), с установленным Apache и ModSecurity, работающим в качестве обратного сервера-посредника по отношению к серверу WebGoat.

12) Запустите Acunetix Web Vulnerability Scanner. Выполните сканирование, для этого нажмите «File->New->Web Site Scan», в появившемся диалоговом окне укажите «Scan Single Website» и адрес сервера (WebSite URL) «http://192.168.10.1/webgoat/attack». Оставляем все настройки по умолчанию, кроме пункта «Login» (указать в параметре «HTTP аутентификация» логин и пароль guest) После завершения всех настроек сканер автоматически начнет тестирование указанного узла.

13) Аналогичным образом просканируйте Web-сервер с установленным ModSecurity. 


Рисунок 1.4 – Результаты сканирования сервера c установленным проектом WebGoat


Рисунок 1.5 – Результаты сканирования сервера c установленным проектом ModSecurity, работающим в качестве обратного прокси-сервера по отношению к серверу с проектом WebGoat.

Проект OWASP WebGoat

Проект предназначенный для изучения web-уязвимостей (т.е. в нем изначально заложены типичные web-уязвимости). Примечателен тем, что развивается в рамках проекта OWASP (Open Web Application Security Project), под эгидой которого выпускается большое количество security-утилит. Данный код написан на Java. Для хостинга J2EE-приложений используется стандартный TomCat-сервер — к счастью, он уже включен в сборку WebGoat и настроен так, чтобы запустить его можно было максимально просто.
Задания, как правило, привязаны к реальной проблеме. Например, в одном из квестов предлагается провести SQL-injection с целью украсть список поддельных кредитных номеров. Некоторые задания сопровождаются учебной составляющей, показывающей пользователю полезные подсказки и уязвимый код.

Данный проект доступен по данному адресу.

Список использованных источников

1. Яремчук С. Как повысить безопасность веб-приложений // Системный администратор. 2006. №2.
2. Ристик И. Защита Web приложений с помощью Apache и ModSecurity // www.securitylab.ru/analytics/216322.php.
3. Юдин А. Защита приложений с помощью ModSecurity // Windows IT Pro. 2007. № 05.
4. Горячие идеи: Фильтрующий прокси // jthotblog.blogspot.com/2009/07/blog-post.html .2009.
5. По данным сайта Microsoft www.microsoft.com
6. По данным сайта Стефана Лэнда www.apachelounge.com
7. По материалам сайта организация The Open Web Application Security Project (OWASP) www.owasp.org/

P.S.: Настройки и синтаксис команд ModSecurity детально описаны в книге Magnus Mischel «ModSecurity 2.5.Securing your Apache installation and web applications»( England: Published by Packt Publishing, 2009. 280 c).

ссылка на оригинал статьи http://habrahabr.ru/post/228103/

Инфраструктура обработки очередей в социальной сети Мой Мир

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

О пользе очередей

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

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

  • В силу того, что каждое событие в сервере очередей обладает временем активации, можно использовать очереди в качестве планировщика отложенных заданий. Таким образом, например, у нас сделаны отложенные посты в группах. Вместо публикации в ленте группы пост укладывается во временное хранилище, а в очереди создается задание со временем активации, равным времени публикации поста.
  • Используя время активации, можно размазывать нагрузку на базы данных во времени, откладывая выполнение некоторых неприоритетных заданий с пикового времени на ночное.
  • Можно при помощи очередей ротировать контент, отображаемый группе пользователей. У нас применяется для социальной модерации фотографий. В специальную очередь складываются изображения, которые необходимо отмодерировать, а вебсервер при отдаче контента пользователю берет одну фотографию из очереди и показывает модератору.
  • Очередь может встраиваться в качестве прослойки в API приема данных, например, push-нотификаций от коллег из соседнего проекта, для повышения надежности и сглаживания нагрузки.
  • Для организации получения данных от внешних источников, например, путем скачивания некоторого контента со стороннего ресурса.
  • Также можно осуществлять принудительную синхронизацию данных с дружественными проектами путем обхода нашей базы и отправки множества запросов к их API. Осуществлять это можно периодически и в течение длительного времени, регулируя нагрузку на API стороннего проекта при помощи ограничения количества обработчиков либо при помощи размазывания времени активации событий.
  • Аналогичным же образом можно осуществлять обход собственных хранилищ с целью исправления данных, побитых вследствие логических ошибок в коде, либо с целью обновления структуры хранимых данных в новый формат.
  • Очень удобно при помощи очередей осуществлять модификации распределенно хранящихся и связанных данных. Для примера можно взять такую операцию как удаление поста, которая также требует удаления всех комментариев и лайков. Соответственно, разбить ее можно на три отдельных очереди: одна отвечает за удаление поста, другая за удаление комментариев, третья за удаление лайков. Таким образом, получаем, что скорость разгребания каждой очереди зависит только от состояния конкретного хранилища и, например, проблемы с хранилищем лайков не повлияют на удаление собственно постов.

Инфраструктура обработки

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

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

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

Схему мы при этом применили достаточно классическую, выделив три компонента: менеджер, мастер и слот.

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

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

Менеджер — это процесс-руководитель кластера, запускается в одном экземпляре (под pacemaker’ом для надежности) и отвечает за весь кластер. Занимается микроменеджментом: на основе данных от сервера очередей и статистических данных от мастеров определяет необходимое количество слотов каждого типа и решает, какой конкретный слот будет заниматься заданиями какого типа.

Управление кластером

Определение необходимого количества слотов для обработки каждого типа очереди менеджером осуществляется на базе:

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

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

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

Кстати, на этой картинке видна пара забавных эффектов, например, что количество слотов (в том числе среднее) может быть меньше минимального. Это означает, что время жизни слота не очень большое, скорее всего в силу интенсивного использования памяти, и они часто перезапускаются. Или то, что загрузка может быть сильно больше 100%, так как она учитывает не только обрабатывающиеся задания, но и те, которые накапливаются в очереди. Именно на основе этого показателя менеджер определяет, что количество слотов надо уменьшать или увеличивать (подсвечивается красным). Для некоторых типов очередей мы сознательно завышаем минимальное количество слотов этого типа. Это необходимо, чтобы обеспечить для этих очередей минимальное время реакции на событие и быть уверенными, что как только оно попадет в очередь, то моментально уйдет в обработку, не ожидая выполнения предыдущих заданий.

Местное самоуправление

Сервера, на которых запускаются мастера, разные по мощности: часть старые и так себе, часть новые и вполне ничего. Поэтому необходимо автоматически, без настроек со стороны админов, определять доступные ресурсы, текущие аппетиты слотов и подстраивать их количество. В нашем случае выяснилось, что самый ценный ресурс — оперативная память. Именно она определяет, сколько слотов можно запустить на сервере. Разумеется, для ее экономии мы постарались максимально использовать copy-on-write и загрузить весь необходимый для исполнения слотов код в мастере с тем, чтобы после форка слотов они всю эту память разделяли. Для того чтобы подсчитать допустимое количество слотов на одном сервере, стандартных VSZ и RSS недостаточно — они не содержат информации о том насколько память процессов расшарена между процессам. К счастью, в linux с некоторых пор имеется замечательный параметр PSS (Proportional Set Size), который для каждой страницы памяти обратно пропорционален количеству процессов, между которыми она пошарена (вычитать и высчитать PSS процесса можно из /proc/$pid/smaps). Сумма PSS мастера и всех слотов, разделенная на количество слотов, примерно соответствует среднему размеру слота (чуть больше, так как на мастер специально не делим). Добавив сумму PSS к свободной памяти и разделив на средний размер слота, можно получить их допустимое количество (минус некоторый запас на всякий случай).

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

Алгоритм обработки заданий


Собственно процесс обработки заданий построен по достаточно простому циклическому алгоритму. Слот постоянно посылает серверу очередей запрос на получение очередной пачки заданий. В случае наличия событий они обрабатываются. При этом по возможности все события из пачки обрабатываются совместно. Это позволяет уменьшить количество запросов к базам данных за счет группировки однотипных и посылки мультизапросов. Либо, в случае отсутствия поддержки мультизапросов со стороны хранилищ, ускорить общение с ними за счет параллелизации асинхронных запросов путем применения libev или libcoro. Логичным решением конечно же было бы вообще использовать асинхронную обработку заданий в рамках event-машины. Но на практике такое решение сопряженно со сложностями при разработке (необходимость выполнения одного и того же кода из-под синхронного веб-сервера и асинхронного обработчика очереди) и отладке.

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

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

Заключение

В заключение хочется отметить, что описываемая инфраструктура используется в Моем Мире не первую пару лет и не требовала какого-либо существенного вмешательства или модификации за это время, даже несмотря на то, что ощутимо выросли и количество очередей, и количество используемых серверов. За это время нам удалось практически свести на нет зоопарк скриптиков и демоночков, переведя их на инфраструктуру менеджера очередей. Ее удобство привело к тому, что новые очереди появляются в проекте не реже, чем новые ajax-функции. И на текущий момент на кластере из 40 серверов (около 3500 обработчиков) за сутки обрабатывается порядка 350 миллионов заданий 150 разных типов.

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

ссылка на оригинал статьи http://habrahabr.ru/company/mailru/blog/228131/

Визуализация игры на сайте и в мобильном

Мобильные технологии продолжают свой головокружительный взлёт, проникая в нашу жизнь плотнее и глубже. Если раньше многое сводилось к простому серфингу из браузера, то сейчас мы стремимся предоставить пользователям полноценные, удобные и красивые приложения использование которых приносит удовольствие и радость. Однако не всё так просто. Большое количество всевозможных устройств с разной производительностью, платформами и разрешениями экранов вынуждают нас искать наиболее адекватные решения при создании продуктов. Приходится учитывать множество факторов: от архитектуры и процессов разработки до вариантов монетизации и способов продвижения. Так или иначе, в необходимости создания мобильных версий или адаптирования существующих приложений уже никто не сомневается, и сегодня мы поговорим о тех задачах которые возникают в связи с этим, а также о способах их решений. В качестве конктретного примера я буду говорить о разработке веб-браузерной игры «Коррупция». В общем случае можно выделить 3 класса задач: дизайн, производительность, кодовая база. Остановимся на каждой из них подробней.
image

Ясно, что просто так разместить на относительно небольшом экране мобильного устройства настольное приложение не получится. Например, интерфейс «Коррупции» изначально ориентировался на 716px в ширину. Если отобразить их даже на iPAD с Retina, это всё равно будет выглядеть не юзабельно. Поэтому дизайн пришлось переделывать полностью. Большинство элементов управления распределились между классическим слайд-меню слева и кнопками в футере. Было принято решение зафиксировать портретную ориентацию экрана, что позволило разместить элементы контента в одну колонку и добиться унифицированного интерфейса как для планшетов, так и для смартфонов. Информация о дополнительных действиях стала отображаться в отдельных окошках, занимающих всю колонку при появлении. Таким образом, удалось добиться относительно вменяемого разделения интерфейса на несколько активных зон: игровое пространство, элементы управления и окошки с дополнительной информацией. Иными словами, игровой интерфейс перестал быть представлен на странице полностью, вместо этого разбился на отдельные блоки, доступ и видимость которых контролирует сам пользователь.

Какие элементы интерфейса показывать в мобильной версии, какие нет — зависит от конкретного приложения. Много важнее как этого добиться. Вероятно, наиболее простым и эффективным способом являются CSS3 Media Queries. Основная идея в определении различных стилей для разных разрешений экранов, например, с помощью директив. Мы делаем резиновую ширину для экранов с разрешением < 960px. Точно также можно добиться различного расположения и видимости элементов контента.
Следующий шаг — шрифты. Если задать их в пикселях, на больших экранах будет слишком мелко. Но и просто так относительную величину задать не получится, потому что нужна исходная метрика — размер шрифта, от которого будут считаться все остальные относительные значения. Поступаем точно также: высчитываем размер базового шрифта в процентах, а затем при загрузке приложения переводим его в абсолютную величину и задаём для body элемента. Таким образом, все дальнейшие размеры шрифта мы можем брать относительно размера базового шрифта.

Теперь о производительности. CSS3 transition позволяет добиться поистине восхитительных эффектов, но мы отказались от них в мобильной версии по причине сильных задержек. На данный момент их рендеринг слишком тяжёлый и медленный. Мы также отказались от классической реализации элементов в DOM-дереве. Если раньше почти всё что нам не нужно было отображать в текущий момент имело свойство display: none; то с мобильными приложениями этот вариант не проходит. Вернее проходит, но тоже с очень большими тормозами, т.к. все невидимые элементы всё равно располагаются в DOM-дереве и создают нагрузку на компонент рендеринга. Решение нашлось в выносе разметки в JavaScript переменные и затем их вставку/удаление из DOM по мере необходимости. То что касается серверной части, здесь всё вполне очевидно. Обмен данными происходит через AJAX, что позволяет значительно снизить количество передаваемых данных. По сути, серверная часть обрастает собственным API, которое можно использовать и для трансформирования десктоп приложения в Single Page Application, если этого по какой-то причине ещё не сделано.

Кодовая база. Все о чём мы тут говорили касается, безусловно, HTML/CSS/JavaScript приложений. Но что если хочется использовать ту же самую кодовую базу, но доставлять продукт через AppStore и GooglePlay? На помощь приходят всевозможные фреймворки, будь то Xamarin или Cordova/PhoneGap. Мы остановились на последнем в силу того, что он использует привычную связку HTML/CSS/JavaScript.

Следующая, пожалуй, самая неприятная проблема — различное поведение. Несмотря на корни идеологии, HTML/CSS/JavaScript все равно местами ведёт себя по-разному на различных платформах. Что-то из этого конфигурируется в самом PhoneGap, но по большей части, единственный способ узнать как поведёт себя приложение — запустить на конечном устройстве. Мы, например, столкнулись с тем, что на iOS, если у блока задано свойство position: fixed, после скрытия клавиатуры, блок не восстанавливал свою высоту. При position: absolute ситуация нормальная. С другой стороны, если постоянно контролировать процесс разработки, регулярно тестируя билды на конечных устройствах, различия в поведении поддаются вменяемой корректировке.

Из приятных моментов — наличие плагинов. Каждый из них обычно написан на языке целевой платформы, предоставляя JavaScript интерфейс. Например, это очень удобно для платежей. Для iOS и Android можно использовать плагины, и хотя интерфейс у них всё же разный, при использовании отдельного объекта адаптера эти различия вполне укладываются в общую картину.

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

ссылка на оригинал статьи http://habrahabr.ru/post/228161/

Планирование аварийного восстановления. Часть третья — заключительная

Соотносим потребности бизнеса с его возможностями

В предыдущих статьях (1,2), посвященных вопросам планирования аварийного восстановления, были описаны процедуры сбора и обработки информации об ИТ-инфраструктуре организации, позволяющие получить точную информацию о:

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

И все бы ничего, если бы не ограниченные финансовые возможности организации, не позволяющие приобрести все необходимые резервы для оперативного восстановления. По этой причине заключительная задача планирования аварийного восстановления – поиск баланса между потребностями и финансовыми возможностями бизнеса, и закрепление его в виде соглашения об уровне обслуживания (Service Level Agreement – SLA) в части устранения возникающих инцидентов.

Данный этап полностью состоит из согласования с руководством компании следующих аспектов взаимодействия:

1. Время поддержки бизнеса внутренней ИТ-службой

Готовность технических специалистов приступить к аварийному восстановлению сразу после получения информации о сбое является основным фактором для определения времени поддержки. Восьмичасовой рабочий день, отпуска, болезни, отгулы естественно ограничивают данную возможность. Если у вас нет специалистов с необходимыми для проведения восстановительных работ компетенциями или нет достаточного перекрытия инженерами как по времени, так и на случай отсутствия одного из них, то бизнесу не стоит рассчитывать на поддержку в графике 24/7. Если же текущее перекрытие специалистами не позволяет гарантировать оперативность реагирования даже в графике 9*5, то тут возможны следующие варианты:

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

Однако и с внешними подрядчиками все не так однозначно:

2. SLA с внешними подрядчиками

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

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

  • Договориться об изменении условий с существующим подрядчиком. Закрепить за собой право на несколько случайных проверок выполнения SLA,
  • Сменить подрядчика на того, чье стандартное SLA соответствует вашим требованиям. И опять же проверять его выполнение,
  • Подключить резервного оператора услуг для оперативного переключения на него в случае проблем у основного,
  • Смириться и оставить все без изменений, если подрядчик является монополистом. Донести данное положение дел до руководства компании и закрепить его с ними,
  • Организовать данный сервис собственными силами.

После того, как вы определились с людьми и/или компаниями, которые будут заниматься восстановительными работами, вы можете обозначить время поддержки пользовательских сервисов, которое может быть заложено в рамки соглашения об уровне обслуживания между ИТ-отделом и бизнесом. Осталось только согласовать предельные сроки их восстановления, а для этого необходимо обсудить:

3. Получение резервов, необходимых для аварийного восстановления

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

  • Приобрести оборудование заранее, если стоимость простоя заведомо превышает их цену. К примеру, резервный коммутатор стоит значительно дешевле простоя на срок его приобретения,
  • Подписать сервисный контракт на замену отказавшего оборудования, если условие «замена на следующий рабочий день» приемлемо для бизнеса,
  • Согласовать оперативное выделение средств на приобретение нужного элемента в случае сбоя, если стоимость простоя сопоставима с резервным элементом,
  • Согласовать снижение качества работы систем в случае возникновения сбоя и/или отключения второстепенных сервисов для запуска в работу бизнес-критичных систем,
  • Согласовать оперативное выделение средств на приобретение менее мощного оборудования для временного запуска в работу отказавшего сервиса с худшими параметрами качества.

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

4. Предварительные заготовки для ускорения аварийного восстановления

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

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

5. Объем выполняемых регламентных задач

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

6. Ситуации, выходящие за рамки SLA.

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

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

На этом этап согласования можно считать завершенным — остались лишь мелкие формальности:

Закрепляем согласованные параметры и действуем

Результаты ваших переговоров с руководством стоит закрепить на бумаге, отразив в ней:

  • Согласованное с бизнесом время поддержки пользовательских сервисов,
  • Гарантируемые сроки восстановления их работы в случае сбоев,
  • Деньги (включая сроки их выделения) и мероприятия, необходимые для достижения поставленных целей,
  • Ситуации, выходящие за рамки планирования и перечень мероприятий, позволяющих уменьшить ущерб в случае их возникновения.

Закрепленные в документе договоренности позволят вам перейти из ситуации когда «ИТ-инфраструктура делает вид что работает, а бизнес делает вид что вкладывает в нее», к ситуации, когда бизнес понимает, на какой уровень сервиса он может рассчитывать в зависимости от инвестиций в ИТ.

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

Успехов!

ссылка на оригинал статьи http://habrahabr.ru/post/228115/

Multiple Delegate

В Cocoa очень популярен паттерн делегирование. Стандартный способ реализации этого паттерна — добавление к делегатору weak свойства, которое хранит ссылку на делегат.

У делегирования много различных применений. Например, реализация какого-то поведения в другом классе без наследования. Еще делегирование используется как способ передачи уведомлений. Например, UITextField вызывает у делегата метод textFieldDidEndEditing:, который информирует его о том, что редактирование закончено, и т.д.

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

Пример

Пример немного притянутый, но все же.

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

Т.е хотим что-то вроде этого:

@protocol PTCTextValidator <NSObject>  - (BOOL)textIsValid:(NSString *)text; - (BOOL)textLengthIsValid:(NSString *)text;  @end  @interface PTCVerifiableTextField : UITextField  @property (nonatomic, weak) IBOutlet id<PTCTextValidator> validator; @property (nonatomic, strong) UIColor *validTextColor UI_APPEARANCE_SELECTOR; @property (nonatomic, strong) UIColor *invalidTextColor UI_APPEARANCE_SELECTOR;  @property (nonatomic, readonly) BOOL isValid;  @end 

И тут возникает проблема. Чтобы PTCVerifiableTextField реализовал кастомное поведение, нужно чтобы он был делегатом своего суперкласса (UITextField). Но если так сделать, то нельзя будет трогать свойство delegate извне.
Т.е. нижеприведенный код поломает внутреннюю логику PTCVerifiableTextField

PTCVerifiableTextField *textField = [PTCVerifiableTextField alloc] initWIthFrame:CGrectMake(0, 0, 100 20)];   textField.delegate = self;   [self.view addSubview:textField];   

Таким образом, получаем задачу: сделать так, чтобы свойству

@property(nonatomic, assign) id<UITextFieldDelegate> delegate   

можно было присвоить несколько объектов.

Решение

Решение напрашивается само собой. Надо сделать контейнер, который будет хранить несколько делегатов и сообщения, которые будут приходить не в него, а в объекты, которые хранятся в контейнере.
Т.е нужен контейнер, проксирующий запросы к хранящимся в нем элементам.

У такого решения есть один большой недостаток — если делегируемая функция возвращает значение, то надо как-то определить, результат вызова какого делегата считать за возвращаемое значение.

Итак, перед тем, как что-то проксировать, надо разобраться, что такое Message Forwarding и NSProxy.

Message Forwarding

Objective-C работает с сообщениями. Мы не вызываем метод на объекте. Вместо этого, мы шлем ему сообщение. Таким образом, под Message Forwarding понимается редирект сообщения другому объекту, т.е. его проксирование.

Важно отметить, что отправка объекту сообщения, на которое он не отвечает, дает ошибку. Однако перед тем, как ошибка будет сгенерирована, рантайм даст объекту еще один шанс, чтобы обработать сообщение.

Давайте рассмотрим, что происходит при отправке объекту сообщения.

1. Если объект реализует метод, т.е можно получить IMP (например, при помощи method_getImplementation(class_getInstanceMethod(subclass, aSelecor))), то рантайм вызывает метод. В противном случае, идем дальше.

2. Вызывается +(BOOL)resolveInstanceMethod:(SEL)aSEL или +(BOOL)resolveClassMethod:(SEL)name, если шлем сообщение классу. Этот метод дает возможность добавить нужный селектор динамически. Если возвращается YES, то рантайм сново пытается получить IMP и вызвать метод. В противном случае, идем дальше.

Еще данный метод вызывается при +(BOOL)respondsToSelector:(SEL)aSelector и +(BOOL)instancesRespondToSelector:(SEL)aSelector, если селектор не реализован. Причем, данный метод вызывается только один раз для каждого селектора, второго шанса добавить метод не будет!

Пример динамического добавления метода:

+ (BOOL)resolveInstanceMethod:(SEL)aSEL {     if (aSEL == @selector(resolveThisMethodDynamically))     {           class_addMethod([self class], aSEL, (IMP) dynamicMethodIMP, "v@:");           return YES;     }     return [super resolveInstanceMethod:aSel]; } 

3. Выполняется так называемый Fast Forwarding. А именно, вызывается метод -(id)forwardingTargetForSelector:(SEL)aSelector
Этот метод возвращает объект, который надо использовать вместо текущего. В общем-то, очень удобная штука для имитации множественного наследования. Fast он, потому что на данном этапе можно сделать форвардинг без создания NSInvoacation.

Для возвращенного этим методом объекта будут повторены все шаги. Согласно документации, если вернуть self, то будет бесконечный цикл. На практике, бесконечного цикла не возникает: видимо, в рантайм внесли поправки.

4. Два предыдущих шага являются оптимизацией форвардинга. После них рантайм создает NSInvocation, вызывая у объекта - (NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector. Если метод вместо NSInvocation вернет nil, то рантайм вызовет у объекта -(void)doesNotRecognizeSelector:(SEL)aSelector, т.е. произойдет крэш.

5. После того, как invocation создан, происходит вызов -(void)forwardInvocation:(NSInvocation *)anInvocation.
Переопределив этот метод, можно передать invocation другому объекту, например так:

- (void)forwardInvocation:(NSInvocation *)invocation {     SEL aSelector = [invocation selector];       if ([friend respondsToSelector:aSelector])         [invocation invokeWithTarget:friend];     else         [super forwardInvocation:invocation]; } 

NSProxy

Из описанного выше следует, что для реализации Method Forwarding достаточно у наследника NSObject переопределить -(NSMethodSignature *)methodSignatureForSelector:(SEL)aSelector и -(void)forwardInvocation:(NSInvocation *)anInvocation:.

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

Реализуем Multiple Delegate

Итак, используя знания о том, как работает Message Forwarding, реализуем наш PTCMultipleDelegate.

Учитывая требования к прокси, получаем для него следующий интерфейс

@interface PTCMultipleDelegate : NSProxy  @property (nonatomic, weak, readonly) id mainDelegate; @property (nonatomic, strong, readonly) NSPointerArray *delegates; @property (nonatomic, assign, readonly) BOOL mainDelegateIsDelegator;  + (instancetype)newProxyForMainDelegate:(id)mainDelegate                               delegates:(NSArray *)delegates                 mainDelegateIsDelegator:(BOOL)mainDelegateIsDelegator;  @end 

Как уже было написано, если делегируемая функция возвращает значение, то надо как-то определить, результат вызова какого делегата считать за возвращаемое значение. Для этой цели служит mainDelegate. Если mainDelegate отвечает на селектор, то в качестве результата будет возвращено значение из mainDelegate, если нет, то значение последнего делегата из delegates.

Выглядит это так:

- (void)forwardInvocation:(NSInvocation *)invocation {     BOOL isMethodReturnSomething = (![[NSString stringWithCString:invocation.methodSignature.methodReturnType                                                          encoding:NSUTF8StringEncoding]                                       isEqualToString:@"v"]);      	if ([self shouldCallMainDelegateForSelector:invocation.selector]) { 		[invocation invokeWithTarget:self.mainDelegate];     } 	 	NSInvocation *targetInvocation = invocation; 	if (isMethodReturnSomething) { 		targetInvocation = [invocation copy]; 	} 	 	for (id delegate in self.delegates) { 		if ([delegate respondsToSelector:invocation.selector]) { 			[targetInvocation invokeWithTarget:delegate];         }     } } 

Метод copy для NSInvocation реализован в категории следующим образом:

@implementation NSInvocation (PTCUtils)  - (instancetype)copy { 	NSInvocation *copy = [NSInvocation invocationWithMethodSignature:[self methodSignature]]; 	NSUInteger argCount = [[self methodSignature] numberOfArguments]; 	 	for (int i = 0; i < argCount; i++) 	{ 		char buffer[sizeof(intmax_t)]; 		[self getArgument:(void *)&buffer atIndex:i]; 		[copy setArgument:(void *)&buffer atIndex:i]; 	}      	return copy; }  @end 

У UITextField есть приватный метод -(void)keyboardInputChangedSelection:(id)selection
Который внутри делает примерно следующее:

- (void)keyboardInputChangedSelection:(id)selection {     if ([self isEditing]) {         if ([self.delegate respondsToSelector:NSSelectorFromString(@"keyboardInputChangedSelection:")]) {             if (self.delegate != self) {                 [self.delegate keyboardInputChangedSelection:selection];             }         }     } } 

Таким образом, если наш наследник UITextField сделает через прокси делегатом самого себя, то приложение может попасть в бесконечный цикл. Чтобы этого избежать, используется свойство mainDelegateIsDelegator. Если его значение YES, то метод из mainDelegate будет вызван только в том случае, если он реализован в сабклассе.

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

BOOL SelectorOverridenInSubclass(Class subclass, SEL aSelecor) {     if (method_getImplementation(class_getInstanceMethod(subclass, aSelecor)) !=         method_getImplementation(class_getInstanceMethod(class_getSuperclass(subclass), aSelecor))) {         return YES;     } else {         return NO;     } } 

Итак, все вместе:

@interface PTCMultipleDelegate ()  @property (nonatomic, weak) id mainDelegate; @property (nonatomic, strong) NSPointerArray *delegates;  @end  @implementation PTCMultipleDelegate  + (instancetype)newProxyForMainDelegate:(id)mainDelegate                               delegates:(NSArray *)delegates                 mainDelegateIsDelegator:(BOOL)mainDelegateIsDelegator; {     return [[self alloc] initProxyMainDelegate:mainDelegate                                      delegates:delegates                        mainDelegateIsDelegator:mainDelegateIsDelegator]; }  - (instancetype)initProxyMainDelegate:(id)mainDelegate                             delegates:(NSArray *)delegates               mainDelegateIsDelegator:(BOOL)mainDelegateIsDelegator {     _mainDelegateIsDelegator = mainDelegateIsDelegator;     _mainDelegate = mainDelegate;     _delegates = [NSPointerArray weakObjectsPointerArray];     [delegates enumerateObjectsUsingBlock:^(id obj, NSUInteger idx, BOOL *stop) {         [_delegates addPointer:(__bridge void *)obj];     }];     return self; }  - (BOOL)shouldCallMainDelegateForSelector:(SEL)aSelector {     if ([self.mainDelegate respondsToSelector:aSelector]) {         if (self.mainDelegateIsDelegator) {             if (SelectorOverridenInSubclass([self.mainDelegate class], aSelector)) {                 return YES;             }         } else {             return YES;         }     }          return NO; }  - (BOOL)respondsToSelector:(SEL)aSelector {     if ([self shouldCallMainDelegateForSelector:aSelector]) {         return YES;     }      for (id delegate in self.delegates) { 		if ([delegate respondsToSelector:aSelector]) { 			return YES;         }     }          return NO; }  - (NSMethodSignature *)methodSignatureForSelector:(SEL)sel {     return [[self.mainDelegate class] instanceMethodSignatureForSelector:sel]; }  - (void)forwardInvocation:(NSInvocation *)invocation {     BOOL isMethodReturnSomething = (![[NSString stringWithCString:invocation.methodSignature.methodReturnType                                                          encoding:NSUTF8StringEncoding]                                       isEqualToString:@"v"]);      	if ([self shouldCallMainDelegateForSelector:invocation.selector]) { 		[invocation invokeWithTarget:self.mainDelegate];     } 	 	NSInvocation *targetInvocation = invocation; 	if (isMethodReturnSomething) { 		targetInvocation = [invocation copy]; 	} 	 	for (id delegate in self.delegates) { 		if ([delegate respondsToSelector:invocation.selector]) { 			[targetInvocation invokeWithTarget:delegate];         }     } }  @end 

Сэмпл

Пример доступен на GitHub.

ссылка на оригинал статьи http://habrahabr.ru/company/e-Legion/blog/227321/