Страх и ненависть в Multipeer Connectivity

ac1fa83948cf41ec8525b80b50c18179.png


Автор: Роман Ивченко, iOS developer DataArt.Введение
Наверняка каждый, кто хоть раз занимался поиском готового решения для обмена сообщениями, файлами, стримами между iOS-устройствами без использования серверной части, слышал о фреймворке Multipeer Connectivity, выпущенном в iOS 7.

Вцелом это один из самых инновационных фреймворков, выпущенных в 7-й версии системы. Он должен был заменить слегка устаревший CoreBluetooth.

Чтобы познать всю мощь и силу Multipeer Connectivity, мы попробовали обкатать его в нашем R&D-проекте, задача которого весьма проста — шаринг презентаций и синхронизация переключения слайдов между устройствами слушателей и устройством докладчика на конференциях, в учебных аудиториях и т. д.

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

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

Докладчик aka Advertiser
c3a1341bf6154907996072cc90c9e1f8.png

Механизм прост. Есть сессия, которая инициализируется параметрами безопасности и шифрования, а также обьектом класса MCPeerID, cущность которого довольно минималистична, т. к. этот класс имеет лишь одно свойство — displayName. Его, по сути, можно считать и названием ID-сессии. Чтобы нашу сессию могли видеть другие пользователи, фреймворк предоставляет нам класс MCAdvertiserAssistant, маяк, сообщающий всем об этой сессии, и хранящий информацию о ней.

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

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

9117cea4347f4cb8afda6c944ed81d3a.png

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

Общение между MCNearbyServiceBrowser и классом контроллера, реализуется через протокол MCNearbyServiceBrowserDelegate, где логика реализованных делегатных методов подразумевает отображение текущих активных сессий адвертайзеров, изменение их состояний.

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

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

First touch
В теории и даже, на первый взгляд, на практике, все смотрится очень удобно с точки зрения реализации архитектуры — разработчик получает доступ только к самому необходимому, избавляя себя от сложной логики работы с сетями wifi/bluetooth.

Как обычно, перед тем как начинать интегрировать фреймворк в проект, все хотят посмотреть, как он работает в официальном демопроекте от Apple. MultipeerGroupChat в принципе показывает, что к чему, и работает довольно стабильно при starter pack-наборе — симулятор и iPhone/iPod/iPad. Разработчики, которые имеют возможность посмотреть на демоприложение, имея больше двух девайсов, сразу могут ощутить: что что-то в этом фреймворке неладно.

Ghost sessions
Первый баг фреймворка который сразу бросается в глаза, даже имея при себе для тестирования всего симулятор и девайс — это проблема призрачных сессий.

Представим ситуацию. Алиса — докладчик (адвертайзер), Боб — слушатель (браузер). Алиса начинает сессию презентации, и Боб подключается к ней, они обмениваются парой сообщений, все нормально. Алиса заканчивает презентацию, и заканчивается ее сессия. Первое — вероятность, что Боб получит нотификацию, что адвертайзера больше нет, — чуть больше вероятности, что не получит.

Для браузер-девайса несуществующая сессия адвертайзера может существовать неопределенное время. При попытке подключения к этой сессии может просто ничего не происходить, или через некоторое время попытка подключения уходит в состояние failed. Проблема обретает серьезные масштабы, если адвертайзер создал свои сессии несколько раз. В таком случае у браузера в нативном контроллере MCBrowserViewController может отображаться несколько одинаковых адвертайзеров iPhone Simulator, когда ты работаешь только с одним девайсом и одним симулятором, и понятия не имеешь, какой адветайзер из списка активен. Пример бага в демоприложении от Apple:

73df326d4dad44589551be065004364f.png

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

Workaround:

Когда у нас имеется только один адвертайзер, каждая новая сессия этого адвертайзера должна иметь discoveryInfo. Это параметр в формате Dictionary, который задается при старте сессии адвертайзера и содержит дополнительную информацию о сессии. Добавив время создания сессии как уникальный идентификатор каждой сессии, на стороне браузера можно отслеживать, какие сессии показывать, а какие нет.

aa24bafe64cb4e52827141897c1990d3.png

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

663fe3a3ab5641588332a7ebe2a00d27.png

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

Workaround:

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

Механизм менеджмента дополнительных сессий на адвертайзере:
414bcdf1063347aca432027a1aa09bfc.png
6b9253157d5243b68e88d3c98c122c44.png
235509b0442b4c89a25d0295ac53ceb2.png

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

Reconnection
Эту головную боль фреймворка я считаю самой затратной по залатыванию костылями. Считаю очень странным со стороны Apple выпустить фреймворк без готового механизма переподключения. Обычный кейс — браузер девайс ушел в слип мод, на протяжении 15–20 секунд он все еще может получать сообщения от адвертайзера, но потом фреймворк сообщает нам о том что connection lost…

Workaround:

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

232e42b457294aba991b0048df288f3c.png

Решение очень грубое, и я уверен, что для некоторых проектов и задач — абсолютно неподходящее, но по-другому это сделать, видимо, невозможно. Пруф, Problem 4.

Общая нестабильность работы, внезапные connection lost

Если предыдущие баги и ограничения еще как-то можно было решить, тут все уже зависит от инженеров Apple. По моим наблюдениям, из 10 попыток тестирования самых простых кейсов для нашего приложения, примерно 7 — 8 заканчивались более-менее успешно. Простые кейсы — примерно 10 — 15% максимально возможной нагруженности приложения (адвертайзер и 20 — 30 браузеров). В остальных фейловых случаях, происходило следующее:

  • Браузер-девайс внезапно перестает получать ообщения от адвертайзера, причем никаких уведомлений от фреймворка, никаких алертов, вызовов делегатных методов — просто тишина.
  • Браузер-девайс внезапно перестает получать сообщения от адвертайзера, кроме сообщений connection lost приходит, причины не уточняются.
  • Браузер-девайс или их группа получают сообщения с серьезной задержкой (на одном девайсе слайд переключился, на другом — только спустя 5 секунд).
  • При попытке реконнекта какого-либо из браузер-девайсов к адвертайзеру начинают отваливаться другие браузеры, или скорость получения ими сообщений от адвертайзера резко падает.

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

748d1825310b4d2fb44ab3e87ca82410.png
a6e3ff2b2ac84dc081c01e22768956a4.png

Увеличение количества подключенных устройств пропорционально уменьшает стабильность всей системы
Эта проблема — главная в статье. Если в случае двух-трех устройств стабильность работы более или менее нормальна, при семи-девяти, не говоря уже о большем количестве, — стабильность начинает стремиться к нулю. Фреймворк просто не работает. Оговорюсь сразу: речь идет о соединении типа «адвертайзер и множество браузеров». Возможно, существуют какие-то другие более стабильные конфигурации, но эта — самая простая в реализации, и при использовании фреймворка хочется, чтобы он работал для всех конфигураций одинаково стабильно.

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

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

Многие сообщения — двухлетней давности, и можно сначала подумать, что все это — детские болезни фреймворка, и в 7 iOS он был внедрен в альфа-версии, но сейчас уже 2016 год, самая последняя версия iOS — 9.2, а проблемы остались все те же.

MultipeerConnectivity крайне не рекомендуется для использования с более чем тремя устройствами. Для двух-трех устройств прогноз благоприятный, но проблем все равно будет много: чтобы добиться того, что, по идее, должно быть «из коробки», нужно потратить вдвое больше времени.
Статью хорошо закончат пару комментариев к теме на devforums.apple.com/message/956192#956192 и ссылка на наше приложение на GitHub github.com/DataArt/SmartSlides.

987937ce5e774570b31a5b6178c8672a.jpg

a4d4dac8d2cf4d00947f70c8d4988ddd.jpg

Подтверждения проблемы:
www.ymc.ch/en/multipeer-connectivity-a-bag-of-hurt
stackoverflow.com/questions/28418965/multipeer-connectivity-vs-real-time-matches
devforums.apple.com/message/956192#956192
devforums.apple.com/message/982861#982861

© Habrahabr.ru