понедельник, 26 октября 2009 г.

ECF: Обмен данными между бандлами с помощью DataShare API


Проголосуйте за ролик первой Российской команды, принявшей участие в конкурсе Imagine Cup Digital Media.

Суровый челябинский программист снова с вами и сегодня мы поговорим об ином аспекте взаимодействия бандлов нежели вызов сервисов - об обмене сообщениями. Под сообщением в данном случае подразумевается произвольный поток байт. Для обеспечения такого взаимодействия в состав ECF входит DataShare API.

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


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

В качестве примера рассмотрим использование DataShare API с помощью контейнеров семейства ecf.generic и контейнера ecf.xmpp.smack, реализующего соединение по протоколу XMPP (напомню, что данный протокол используется в Jabber).

Оба этих типа контейнеров (ecf.generic.client и ecf.xmpp.smack) не могут взаимодействовать напрямую, а только посредством сервера. В случае ecf.xmpp.smak сервером является любой XMPP-сервер, например gmail.com, ya.ru и т.д. В случае ecf.generic.client сервером является контейнер типа ecf.generic.server, который надо предварительно создать:

public static final String GENERIC_SERVER_ID = "ecftcp://localhost:4280/mygroup";



public static final String GENERIC_SERVER_CONTAINER = "ecf.generic.server";





private IContainer createServer() throws Exception

{

    return ContainerFactory.getDefault().createContainer(GENERIC_SERVER_CONTAINER, getNewID(GENERIC_SERVER_ID));

}



public void start(BundleContext context) throws Exception

{

    _container = createServer();

    // ...

}


Метод gentNewID(String id) будет использоваться во всех примерах, код его тривиален:

private ID getNewID(String id) throws IDCreateException

{

    return IDFactory.getDefault().createStringID(id);

}


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

Общая последовательность работы с DataShare API такова:

1. Создаем контейнеры (например, ecf.generic.client

public static final String GENERIC_CLIENT_CONTAINER = "ecf.generic.client";



//...



containers = new IContainer[CLIENTS_CNT];



for (int i = 0; i < containers.length; i++)

    containers[i] = ContainerFactory.getDefault().createContainer(GENERIC_CLIENT_CONTAINER);

 


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

public static final String CHANNEL_NAME = "channel";



for (int i = 0; i < CLIENTS_CNT; i++)

{

    IChannelContainerAdapter channelContainer = (IChannelContainerAdapter) containers[i]

                    .getAdapter(IChannelContainerAdapter.class);

    channelContainer.createChannel(getNewID(CHANNEL_NAME), getChannelListener(containers[i].getID()), null);

}


Один контейнер может иметь только один канал с заданным ID. Т.е. нельзя регистрировать два канала с одним и тем же ID для одного и того же контейнера.

Для создания канала используется IChannelContainerAdapter. Чтобы при создании данного адаптера не было NPE, необходимо присутствие бандла org.eclipse.ecf.provider.datashare в конфигурации запуска.

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

private Hashtable<ID, IChannelEvent> messageEvents = new Hashtable<ID, IChannelEvent>();



private IChannelListener getChannelListener(final ID id) throws Exception

{

    return new IChannelListener()

    {

        public void handleChannelEvent(IChannelEvent event)

        {

            if (event instanceof IChannelMessageEvent)

            {

                messageEvents.put(id, event);

            }

        }

    };

}


3. Подключаемся к серверу.

Подключение к серверу осуществляется вызовом метода connect контейнера клиента. Данный метод получает два параметра: ID сервера и так называемый контекст соединения. Контекст соединения может служить, например, для авторизации. В нашем случае авторизация не нужна, поэтому можно передать null.

public static final String GENERIC_SERVER_ID = "ecftcp://localhost:4280/mygroup";



private void connectClients() throws Exception

{

    for (int i = 0; i < containers.length; i++)

        connectClient(containers[i], IDFactory.getDefault().createStringID(GENERIC_SERVER_ID), getConnectContext(i));

}



private IConnectContext getConnectContext(int index)

{

    return null;

}



private void connectClient(IContainer containerToConnect, ID connectID, IConnectContext context)

        throws ContainerConnectException

{

    containerToConnect.connect(connectID, context);

}

 


4. Отправляем сообщение.

Сообщение отправляется с помощью метода IChannel#sendMessage, который может быть вызван с одним обязательным параметром: массивом байт, который будет передан по каналу. В таком случае этот массив байт получат все контейнеры, которые создали канал с соответствующим ID. Другим вариантом может быть вызов метода IChannel#sendMessage с переданным в качестве первого параметра ID контейнера-получателя. В таком случае, сообщение будет передано только этому контейнеру.

private void sendMessages() throws IDCreateException, Exception

{

    IChannelContainerAdapter senderContainer = getChannelContainer(0);



    IChannel sender = senderContainer.getChannel(getNewID(CHANNEL_NAME));

    sender.sendMessage("Hello from ECF!".getBytes());



    sleep(3000);



    for (int i = 1; i < CLIENTS_CNT; i++)

        handleEvent(containers[i].getID());

}



private void handleEvent(ID id)

{

    IChannelEvent event = messageEvents.get(id);

    if (event instanceof IChannelMessageEvent)

    {

        System.out.println("chanelID = " + event.getChannelID().getName());

        System.out.println("data = " + new String(((IChannelMessageEvent) event).getData()));

        System.out.println();

    }

}


Теперь вернемся к серверу. Во-первых, сервер имеет возможность обрабатывать момент соединения с ним клиента. Для этого нужно добавить к контейнеру сервера обработчик события IContainerConnectedEvent. Для добавления обработчиков событий у IContainer есть метод addListener:

// ...

_container.addListener(createConnectedContainerListener());

// ...



private IContainerListener createConnectedContainerListener()

{

    return new IContainerListener()

    {

        public void handleEvent(IContainerEvent event)

        {

            if (event instanceof IContainerConnectedEvent)

            {

                try

                {

                    ID containerId = ((IContainerConnectedEvent) event).getTargetID);

                    System.out.println("connected from " + containerId.getName());

                }

                catch (Exception e)

                {

                    e.printStackTrace();

                }

            }

        }

    };

}


В данном примере сервер будет реагировать на каждое подключение, выводя уведомление в OSGi-консоль.

Во-вторых, сервер может как получать сообщения от клиента, так и посылать их ему. Для этого можно в момент соединения создать канал для связи клиент-сервер. Клиент может по этому каналу отправить сообщение на которое сервер ответит (см. примеры к статье).

Интересным примером может быть организация обмена данными с помощью XMPP-протокола. Такой вариант имеет свои особенности.

1. ID сервера задается в пространстве имен ecf.xmpp и представляет собой корректный Jabber Account ID, который будет использоваться.

public static final String XMPP_NAMESPACE = "ecf.xmpp";



// ...

return IDFactory.getDefault().createID(IDFactory.getDefault().getNamespaceByName(XMPP_NAMESPACE),

    "samolisov@gmail.com");


2. При соединении обязательно нужно передать контекст соединения, т.е. пользовательскую пару логин/пароль. Создается контекст соединения следующим образом:

ConnectContextFactory.createUsernamePasswordConnectContext(username, password);


3. ID канала задается в пространстве имен по-умолчанию и может называться как угодно, например, просто channel.

4. При отправке сообщения необходимо обязательно указывать ID получателя, заданное относительно пространства имен ecf.xmpp. Чтобы не было проблем, нужно указывать полный уникальный ID ресурса (т.е. что-то вроде "samolisov@gmail.com/Gajim12345"). Я сделал следующую ошибку: не указал ID ресурса, указал только samolisov@gmail.com, но у меня было запущенно два Джаббер-клиента, в итоге сервер (gmail.com) не смог выбрать куда ему отправить сообщение.

5. Вполне допускается и корректно работает отправка данных между разными Jabber-серверами. Однако, gmail.com, например, не позволяет отправлять сообщение на JID, которого нет в ростере. Поэтому перед тестированием/использованием добавьте JID получателя в ростер отправителя.

6. Существует замечательный инструмент отладки. Если при запуске вашего приложения указать Java-машине параметр -Dsmack.debugEnabled=true, то будет показано окно, отображающее все XMPP-пакеты, проходящие через ваш аккаунт.

Тема взаимодействия бандлов очень интересна. ECF позволяет не только обмениваться потоками байт, но и произвольными Java-объектами, однако, об этом мы поговорим в другой раз.

Оставайтесь на связи.

Скачать примеры к статье (исходники и конфигурации запуска. Rar, 16 Кб)

Понравилось сообщение - подпишитесь на блог или читайте меня в twitter

Комментариев нет:

Отправить комментарий

Любой Ваш комментарий важен для меня, однако, помните, что действует предмодерация. Давайте уважать друг друга!