Peer-to-Peer Chat in Flash
Этой статьей я хочу открыть новую тему в своем блоге, посвященную разработке на Flash/Flex/ActionScript3. Итак приступим.
Flash Player 10.0
Начиная с версии 10.0 во Flash Player появилась очень интересная возможность, которая позволяет устанавливать прямые соединения между плеерами при помощи сервиса Stratus:
1 2 3 | nc = new NetConnection(); nc.addEventListener(NetStatusEvent.NET_STATUS, netStatusHandler); nc.connect('rtmfp://stratus.adobe.com/YOUR_DEVELOPER_KEY_HERE'); |
Этот сервис выполняет роль организатора: подключившись к нему ваш клиент получает уникальный Peer ID, зная который, другие такие же как и вы клиенты, могут напрямую подключаться к вашему клиенту и подписываться на данные, которые вы публикуете.
Давайте рассмотрим эту схему более подробно, по шагам:
- Flash-клиент в вашем браузере соединяется с сервисом Stratus
- В клиенте создается поток (NetStream), в который начинается публикация какого-то контента
- Другие flash-клиенты также соединяются с сервисом Stratus
- В них создаются потоки, которые подключаются к вашему клиенту по его Peer ID и подписываются на контент от него
Это самая простая схема: один клиент публикует контент, а другие подписываются на получение контента.
Но давайте попробуем усложнить эту схему. Что если каждый клиент и публикует и подписывается на потоки всех известных ему клиентов?
Чат
Сейчас я много занимаюсь разработкой приложений для коммуникации. Это и аудио/видео чаты, и обычные текстовые чаты (кто-нибудь помнит мой Mel.Chat?). И в связи с этим я хочу поделиться небольшим примером разработки простого текстового чата, основанного на технологии Peer-to-Peer или P2P. В отличии от старичка Mel.Chat’а, который основан на постоянной долбежке сервера AJAX-запросами, в этом чате все очень похоже на технологию клиент/сервер. Создаются необходимые соединения, которые просто остаются открыты столько сколько нужно и просто ожидают данные, которые отправляют (публикуют) другие клиенты. Но если с технологией клиент/сервер все более или менее понятно (все клиенты соединяются с одним определенным сервером, и все коммуникации осуществляются путем отправки сообщений на сервер, который в свою очередь отправляет сообщения всем подключенным к нему клиентам), то с P2P у нас так не получится, потому что тут у нас нет какого-то определенного сервера, а каждый отдельный клиент должен быть подписан на всех других участвующих в чате клиентов.
Схема ясна, переходим к практике.
Сервис регистрации Peer ID
Во-первых необходимо организовать сервис обмена Peer ID (далее фингерпринт) между клиентами. Это нужно для того, чтобы каждый новый подключившийся клиент мог самостоятельно найти хотябы одного активного в данный момент клиента и установить с ним соединение. Ниже представлен простейший код такого сервиса на PHP & MySQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | <?php require 'lib/db.php'; Db::connect('localhost', 'user', 'password', 'database') or die(Db::error()); define('DB_TABLE', 'p2pchat'); $time = time(); $cooldown = 900; // 15 min $limit = 5; // удаляем все истекшие записи Db::delete(DB_TABLE, 'timestamp < '.($time - $cooldown)); if (isset($_GET['insert']) && ! empty($_GET['insert'])) { // выбираем последние обновленные записи для попытки соединения с ними $fingerprints = Db::select(DB_TABLE, 'fingerprint', NULL, 'timestamp DESC', $limit); // записываем в базу свой фингерпринт Db::insert(DB_TABLE, array('fingerprint' => Db::escape($_GET['insert']), 'timestamp' => $time)); // отправляем в ответ выбранные записи if (sizeof($fingerprints) > 0) { $response = array(); foreach ($fingerprints as $fp) { $response[] = $fp['fingerprint']; } echo implode(',', $response); } } else if (isset($_GET['update']) && ! empty($_GET['update'])) { // обновляем свой фингерпринт Db::update(DB_TABLE, array('timestamp' => $time), 'fingerprint = "'.Db::escape($_GET['update']).'"'); } |
Давайте я немного расскажу о принципе работы этого сервиса (или трекера). После того как ваш flash-клиент установил соединение с сервисом Stratus он отправляет GET-запрос с вашим Peer ID в качестве параметра сервису регистрации фингерпринтов. Ваш фингерпринт сохраняется в базе с текущим временем. Также сервис выбирает из базы несколько последних добавленных/обновленных фингерпринтов и отправляет их в ответ вашему клиенту. Клиент пытается установить соединение с каждым из полученных фингерпринтов:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | var loader:URLLoader = new URLLoader(); loader.addEventListener(Event.COMPLETE, function(e:Event):void { log('Insert fingerprint complete'); var fps:Array = e.target.data.split(','); // перебираем все полученные фингерпринты, // и пытаемся с ними соединиться for (var i:int = 0; i < fps.length; i++) { if (fps[i].length) { initRecvStream(fps[i]); } } }); try { loader.load(new URLRequest('http://url/to/p2pchatreg.php?insert='+myPeerID)); } catch (error:Error) { log('Unable to insert my peer id'); } |
И тут начинается самое интересное…
Оповещение о новом участнике
Так как запрос регистрации возвращает ограниченное количество последних обновленных фингерпринтов, наш клиент не сможет подключиться ко всем участвующим в чате клиентам, но этого и не потребуется, потому что клиенты сами “рассказывают” друг другу о подключении новых участников к чату, а также оповещают подключающихся к ним клиентах о своих именах:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | client.onPeerConnect = function(subscriber:NetStream):Boolean { // когда ко мне присоединяется подписчик, проверяем, // если это новый подписчик, то оповещаем всех о нем, // а также подписываемся на его поток if (! hasSubscriptionTo(subscriber.farID)) { sendStream.send('onBroadcastNewSubscriber', subscriber.farID); initRecvStream(subscriber.farID); } // ... и конечно же оповещаем его о своем имени subscriber.send('onPeerNameUpdate', myPeerID, myName); return true; }; |
Итак, подключившись хотя бы к одному активному участнику чата или пиру (от англ. peer), тот немедленно проверяет свой список подключенных к нему пиров, и если вашего клиента с его уникальным peer id там еще нет, то он оповещает всех о новом подключенном к конференции клиенте, и все уже связанные друг с другом пиры сами устанавливают соединение с вами. Таким образом образуется самоформирующаяся группа участников конференции (во Flash Player 10.1 всю эту механику берет на себя новый класс NetGroup + GroupSpecifier).
Актуальность фингерпринтов
Актуальность активных в данный момент фингерпринтов – это ключевой момент всего приложения. Чтобы поддерживать актуальность своего фингерпринта каждый активный клиент через определенный промежуток времени отправляет GET-запрос на сервис регистрации фингерпринтов с просьбой обновить время активности своего фингерпринта:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | // запускаем таймер который будет тикать каждые 5 минут (300 секунд) var timer:Timer = new Timer(300 * 1000); timer.addEventListener(TimerEvent.TIMER, function():void { var loader:URLLoader = new URLLoader(); loader.addEventListener(Event.COMPLETE, function(e:Event):void { log('Update fingerprint complete'); }); try { loader.load(new URLRequest('http://url/to/p2pchatreg.php?update='+myPeerID)); } catch (error:Error) { log('Unable to update my peer id'); } }); timer.start(); |
Таким образом новые участники конференции всегда получают пачку самых последних активных пиров на данный момент. И подключившись хотя бы к одному активному пиру, тот в свою очередь, оповещает всех о новом участнике.
Заключение
В итоге, у нас получается достаточно простой чат, который не использует сервер для передачи сообщений, и поддерживает большое количество клиентов онлайн. Мгновенно отправляет и получает сообщения. К такому чату можно добавить аудио/видео звонки и передачу файлов (аля Skype). Основными требованиями для работы такого чата являются наличие установленного Flash Player 10.0+ и разрешенный UDP-траффик.
В следующей статье я расскажу об использовании более удобных средств для организации групп при помощи классов NetGroup и GroupSpecifier из FlashPlayer 10.1.
Если вы хотите быть в курсе моих новых публикаций о flash и peer-to-peer, то не стесняйтесь и подписывайтесь на RSS-ленту моего блога или следите за мной в Twitter.
Живой пример P2PChat – Чтобы опробовать чат откройте его в 2х или более окнах браузера, расположите их рядом друг с другом и попробуйте что-нибудь написать.
Скачать исходные коды P2PChat – Для Flash Builder 4 + Flex SDK 4. Для работы со Стратусом вам необходимо получить “stratus developer key” зайдя на страницу Stratus и перейдя по ссылке “Signup for a Stratus beta developer key”.

Спасибо, весьма занимательно.
Подумывая заняться флэшем, хотелось бы узнать, есть ли возможность организовать свой рандеву
сервис, в обход Stratus?
Живой пример пример не работает, в логе висит Connecting… и все…
Также хотелось бы сравнений, например с jxta?
Я думаю “свои” рандеву сервисы не за горами. Живой пример работает отлично, но возможно у вас перекрыт udp-траффик или еще что-то, чтобы понять – нужно прикручивать ловушку для ошибок. По поводу сравнения с jxta сказать ничего не могу, так как с java я знаком очень посредственно.
Ok, спасибо за ответ. Открыл 1935 порт – заработало. По поводу рандеву – единственное, что пока удалось найти – в зачатии проект Blue5 с поддержкой rtmfp. Но, как разаработчики пишут, – у них сейчас есть более важные дела и займутся они этим не ранее, чем выйдет red5 1.0 …
Melnaron, скажите пожалуйста, эта технология на самом деле работает p2p – т.е. связь осуществляется напрямую между клиентами, или все-таки все соединения идут через шлюз (через сервера Адоби) ? Потому как я сижу за роутером, который не разрешает входящих соединений, тем не менее ваш “живой пример чата” у меня работает. Я к чему это спрашиваю – для чата может и пойдет такое соединение, но если допустим рассылать видео, то наверное пропускная способность сервера Адоби может вызвать тормоза и сбои (или вообще ограничено количество таких подключений на стороне сервера)?
Да, Oobe, эта технология – это действительно самый настоящий Peer-to-Peer. Сервер Эдоби – Cirrus (ранее Stratus) – только помогает пирам установить соединение друг с другом. После установления соединения между двумя и более пирами весь траффик уже гоняется исключительно только между ними. Соответственно если вы хотите передавать видео, то оно будет напрямую передаваться всем соединенным с вами пирам. Но при передаче видео большому количеству подписчиков встает другой вопрос: пропускная способность вашего канала на отдачу… В этом случае придется очень кстати Cirrus 2 с его application-level multicast (http://labs.adobe.com/technologies/cirrus/).