Peer-to-Peer Chat in Flash

May 30th, 2010 | Categories: Development | Tags: , , ,

Этой статьей я хочу открыть новую тему в своем блоге, посвященную разработке на 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”.

  1. xmlelement
    June 29th, 2010 at 10:38
    Quote | #1

    Спасибо, весьма занимательно.
    Подумывая заняться флэшем, хотелось бы узнать, есть ли возможность организовать свой рандеву
    сервис, в обход Stratus?
    Живой пример пример не работает, в логе висит Connecting… и все…
    Также хотелось бы сравнений, например с jxta?

    • June 29th, 2010 at 15:51
      Quote | #2

      Я думаю “свои” рандеву сервисы не за горами. Живой пример работает отлично, но возможно у вас перекрыт udp-траффик или еще что-то, чтобы понять – нужно прикручивать ловушку для ошибок. По поводу сравнения с jxta сказать ничего не могу, так как с java я знаком очень посредственно.

  2. xmlelement
    June 30th, 2010 at 11:09
    Quote | #3

    Ok, спасибо за ответ. Открыл 1935 порт – заработало. По поводу рандеву – единственное, что пока удалось найти – в зачатии проект Blue5 с поддержкой rtmfp. Но, как разаработчики пишут, – у них сейчас есть более важные дела и займутся они этим не ранее, чем выйдет red5 1.0 …

  3. Oobe
    November 27th, 2010 at 08:01
    Quote | #4

    Melnaron, скажите пожалуйста, эта технология на самом деле работает p2p – т.е. связь осуществляется напрямую между клиентами, или все-таки все соединения идут через шлюз (через сервера Адоби) ? Потому как я сижу за роутером, который не разрешает входящих соединений, тем не менее ваш “живой пример чата” у меня работает. Я к чему это спрашиваю – для чата может и пойдет такое соединение, но если допустим рассылать видео, то наверное пропускная способность сервера Адоби может вызвать тормоза и сбои (или вообще ограничено количество таких подключений на стороне сервера)?

    • November 27th, 2010 at 13:18
      Quote | #5

      Да, Oobe, эта технология – это действительно самый настоящий Peer-to-Peer. Сервер Эдоби – Cirrus (ранее Stratus) – только помогает пирам установить соединение друг с другом. После установления соединения между двумя и более пирами весь траффик уже гоняется исключительно только между ними. Соответственно если вы хотите передавать видео, то оно будет напрямую передаваться всем соединенным с вами пирам. Но при передаче видео большому количеству подписчиков встает другой вопрос: пропускная способность вашего канала на отдачу… В этом случае придется очень кстати Cirrus 2 с его application-level multicast (http://labs.adobe.com/technologies/cirrus/).

Comments are closed.