[kde-russian] DCOP HOWTO

=?iso-8859-1?q?mok_=CE=C1_kde=2Eru?= =?iso-8859-1?q?mok_=CE=C1_kde=2Eru?=
Пт Дек 14 23:10:02 MSK 2001


По моей просьбе Илья Яловой сделал перевод документа, описывающего
работу DCOP.

Вот его перевод в моей редакции. Оригинал лежит в каталоге
kdelibs/dcop.

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

Гриша Мохин

==================================================================

DCOP: Протокол взаимодействия приложений в рабочей среде
(DCOP: Desktop COmmunications Protocol)

Автор - Preston Brown <pbrown на kde.org>, October 14, 1999
С изменениями и дополнениями - Matthias Ettrich <ettrich на kde.org>, Mar 29, 2000
Обзор cигналов DCOP - Waldo Bastian <bastian на kde.org> Feb 19, 2001

Перевод на русский - Ilya V. Yalovoy <i_yalovoy на mail.ru> Nov 20, 2001

Общие положения:
----------------

Причины создания протокола DCOP просты. Прошлый год ознаменовался
попытками обеспечить межпроцессное взаимодействие приложений KDE. В
KDE 1.х на тот момент уже существовал предельно простой IPC-механизм
под названием KWMcom, который, например, применялся для взаимодействия
между панелью и менеджером окон. Этот протокол был прост насколько это
возможно и для передачи сообщений использовал X Atoms. По этой причине
он имел ограничения по размеру и сложности передаваемых сообщений (а X
Atoms обязаны быть маленькими, чтобы сохранить высокую
производительность) и был тесно завязан на сам X-сервер. Мы думали в
качестве более эффективного IPC/RPC-решения применить технологию
CORBA. Попытки ее использовать в KDE длились около года, и в конце
концов стало очевидно, что CORBA слишком медленная и требовательная к
памяти даже при решении простых задач. Кроме того, в ней не
предусмотрена аутентификация.

Что нам действительно было нужно, так это предельно простой протокол с
базовой поддержкой аутентификации, типа MIT-MAGIC-COOKIE (применяется
в Х). Он не должен обладать всей мощью CORBA, но его возможностей
должно хватать для решения простых задач. Примером такой задачи может
служить отправка панели задач сообщения "Приложение запущено успешно -
прекратить отображение состояния как 'application starting'" или дать
возможность приложению при запуске проверить, не запущены ли уже его
копии. Если приложение уже запущено, то можно просто вызвать его
функцию и открыть новое окно, а не запускать отдельный процесс.

Реализация:
-----------

DCOP - простой IPC/RPC-механизм, спроектированный для работы через
сокеты. В нем поддерживаются как Unix-сокеты, так и сокеты TCP/IP.
DCOP создан на основе Inter Client Exchange (ICE) протокола, который
входит в X11R6 и более поздние версии. Он также зависит от Qt, но не
требует больше никаких дополнительных библиотек. Этим объясняется
легкость интеграции этой новой технологии во все приложения KDE и
малые затраты ресурсов.

Модель:
-------

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

С помощью DCOP можно выполнять два вида запросов:
- "send and forget" - "послать и забыть" - отправляет сообщение, не блокируя процесс;
- "calls" - "запросы" - отправляет сообщение и блокирует процесс до получения ответа.

Вся информация передаваемая с помощью DCOP подлежит сериализации (для
тех, кто любит говорить на языке CORBA, - последовательной
диспетчеризации (marshalling)) с помощью встроенного во все классы Qt
оператора QDataStream. Это просто и быстро. На самом деле, это
настолько простая процедура, что вы легко можете ее написать вручную.
Кроме этого, существует IDL-подобный компилятор (dcopidl и
dcopidl2cpp), позволяющий автоматически генерировать специальные
заготовки. Вы обеспечите дополнительную безопасность типов, используя
компилятор dcopidl.

В этом HOWTO в первую очередь описывается ручной метод, а затем
затрагивается использование компилятора dcopidl.

Установление связи:
-------------------

KApplication содержит метод "KApplication::dcopClient()", который
возвращает указатель на экземпляр DCOPClient. При первом вызове этого
метода создается класс клиента. Клиенты DCOP имеют уникальные
идентификаторы, основанные на результате вызова KApplication::name().

Чтобы начать собственно работу через DCOP, необходимо вызвать метод
DCOPClient::attach(). Этот метод пытается установить связь с
DCOP-сервером. Если сервер не найден или произошла другая ошибка при
подключении, то attach() возвращает значение false. KApplication может
отследить сигналы DCOP и отобразить соответствующее сообщение об
ошибке.

После подключения к серверу с помощью DCOPClient::attach() вы должны
зарегистрировать appId приложения на сервере, чтобы он знал о вашем существовании.
Иначе вы будете работать анонимно. Для регистрации используется метод
DCOPClient::registerAs(const QCString &name). В простейшем случае:

/*
 * Возвращает реально зарегистрированный appId, который _может_
 * отличаться от переданного значения
 */
appId = client->registerAs(kApp->name());

Если вы не запрашиваете указатель на DCOPClient у KApplication, то
соответствующий объект не создается и дополнительного расхода памяти не
происходит.

Вы также можете отключиться от сервера с помощью метода DCOPClient::detach().
Если вам надо восстановить соединение с сервером, то регистрацию придется
повторить. Если вы хотите только изменить свой ID, то достаточно вызвать метод
DCOPClient::registerAs() с указанием нового имени.

KUniqueApplication автоматически регистрирует себя в DCOP. Если вы используете
KUniqueApplication, то установление связи и регистрация выполняются
автоматически, без необходимости явно вызывать функции.
По умолчанию в качестве appId используется результат функции
kapp->name(). Вы можете найти зарегистрированного клиента DCOP с
помощью функции kapp->dcopClient().

Передача данных между приложениями:
--------------------------------------

Существует два способа взаимодействия. Они заключаются в
использовании методов "send" или "call". Оба метода требуют три
идентификационных параметра:
- идентификатор приложения;
- удаленный объект;
- удаленная функция.
Для асинхронной работы необходимо использовать метод "send" (т.е. управление
возвращается сразу) при этом результат может быть получен в какой-то момент в
будущем. Возможно, что результат вообще не будет возвращен. К тому же, для вызова
"send" необходимо указать один, а для "call" - два параметра.

Удаленный объект определяется как иерархия объектов. При этом объект верхнего
уровня называется "fooObject", а его потомок - "barObject", вы должны
обращаться к необходимому объекту как "fooObject/barObject".
Функции должны объявляться в полной форме записи (своей полной сигнатуре).
Если удаленная функция
называется "doIt", и в качестве аргумента она должна получить int, то она должна
быть описана как "doIt(int)". Обратите внимание, что здесь не указывается тип
возвращаемого результата, потому что он не является частью сигнатуры функции (во
всяком случае по идеологии С++). Вы получаете тип результата в качестве
дополнительного параметра метода DCOPClient::call(). Подробней смотрите раздел,
посвященный функции call().

Для передачи удаленному клиенту данные должны быть "сериализованы" с помощью
QDataStream, который работает с QByteArray. Рассмотрим несколько примеров, чтобы
понять, как формируются параметры данных.

Допустим, вы хотите выполнить функцию "doIt", как было описано выше, при этом не
блокируя процесс (не ожидая результата). С одной стороны, вы не получите
результата от удаленно выполненной функции, но с другой стороны, процесс не будет
блокирован до завершения выполнения вызова RPC. Значение, возвращенное функцией send(),
свидетельствует об ошибках (или их отсутствии) самого протокола DCOP.

QByteArray data;
QDataStream arg(data, IO_WriteOnly);
arg << 5;
if (!client->send("someAppId", "fooObject/barObject", "doIt(int)",
                  data))
  qDebug("there was some error using DCOP.");

Хорошо, теперь давайте рассмотрим пример, когда нам необходимо получить
результат от удаленной функции. Для этого необходимо использовать метод
call() вместо send(). Возвращаемое значение будет доступно через параметр
"reply". Значение, возвращаемое методом call(), имеет тот же смысл, что и у метода
send(), то есть указывает на результат работы DCOP.

QByteArray data, replyData;
QCString replyType;
QDataStream arg(data, IO_WriteOnly);
arg << 5;
if (!client->call("someAppId", "fooObject/barObject", "doIt(int)",
                  data, replyType, replyData))
  qDebug("there was some error using DCOP.");
else {
  QDataStream reply(replyData, IO_ReadOnly);
  if (replyType == "QString") {
    QString result;
    reply >> result;
    print("the result is: %s",result.latin1());
  } else
    qDebug("doIt returned an unexpected type of reply!");
}

ВНИМАНИЕ: Нельзя вызывать с помощью call() метод, принадлежащий приложению,
зарегистрированному с уникальным цифровым id, добавленным к его текстовому
названию (подробней см. dcopclient.h). В этом случае, DCOP не будет знать, с
каким приложением необходимо установить связь, чтобы вызвать указанный метод.
Такой проблемы не возникает при использовании send(), так как вы можете послать
широковещательный запрос всем приложениям, зарегистрированным с идентификатором
вида appname-<numeric_id>, используя шаблон имени (типа 'konsole-*'),
который посылает ваш сигнал всем приложениям с именем 'konsole'.

Прием данных с помощью DCOP:
----------------------------

На данный момент единственным способом получить данные от DCOP является
множественное наследование от необходимого вам нормального класса (обычно один
из производных классов QWidget или QObject) и класса DCOPObject. В DCOPObject есть
очень важный метод: DCOPObject::process(). Это чисто виртуальный метод, который
необходимо переопределить, чтобы обеспечить обработку принимаемых сообщений DCOP.
Этот метод получает сигнатуру функции, QByteArray параметров и указатель на
результирующий массив данных QByteArray, который необходимо заполнить.

Относитесь к DCOPObject::process(), как к своего рода диспетчеру. В будущем,
возможно, будет использоваться предкомпилятор для добавления этого метода
автоматически. Однако пока вам необходимо самостоятельно обрабатывать входящие
сигнатуры функций и предпринимать адекватные действия. Вот пример такой
обработки:

bool BarObject::process(const QCString &fun, const QByteArray &data,
                        QCString &replyType, QByteArray &replyData)
{
  if (fun == "doIt(int)") {
    QDataStream arg(data, IO_ReadOnly);
    int i; // parameter
    arg >> i;
    QString result = self->doIt (i);
    QDataStream reply(replyData, IO_WriteOnly);
    reply << result;
    replyType = "QString";
    return true;
  } else {
    qDebug("unknown function call to BarObject::process()");
    return false;
  }
}

Обработка вызовов call():
-------------------------

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

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

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

bool BarObject::process(const QCString &fun, const QByteArray &data,
                        QCString &, QByteArray &)
{
  if (fun == "doIt(int)") {
    QDataStream arg(data, IO_ReadOnly);
    int i; // parameter
    arg >> i;
    QString result = self->doIt(i);

    DCOPClientTransaction *myTransaction;
    myTransaction = kapp->dcopClient()->beginTransaction();

    // start processing...
    // Calls slotProcessingDone when finished.
    startProcessing( myTransaction, i);

    return true;
  } else {
    qDebug("unknown function call to BarObject::process()");
    return false;
  }
}

slotProcessingDone(DCOPClientTransaction *myTransaction, const QString &result)
{
    QCString replyType = "QString";
    QByteArray replyData;
    QDataStream reply(replyData, IO_WriteOnly);
    reply << result;
    kapp->dcopClient()->endTransaction( myTransaction, replyType, replyData );
}

Сигналы DCOP:
-------------

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

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

Источником сигналов DCOP является комбинация DCOP Object/DCOP Client
(отправитель). Он может быть подключен к функции другого DCOP Object/DCOP Client
(приемник).

Подключение сигналов DCOP имеет две основных особенности: в отличие от
Qt, подключения сигналов DCOP могут иметь анонимного отправителя, и кроме того,
подключения сигналов DCOP могут быть долговременными (non-volatile).

С помощью DCOP сигналы могут быть подключены без указания объекта-отправителя
и/или DCOP-клиента. В этом случае будут приниматься сигналы от любого объекта
и/или клиента DCOP. Это позволяет определить события без привязки к конкретному
объекту, реализующему это событие.

Другой особенностью DCOP являются так называемые долговременные (non-volatile)
соединения. Подключения сигналов Qt удаляются, в случае если удаляется приемник или
передатчик сигнала. Временные (volatile) подключения сигналов DCOP работают
аналогично. Тем не менее, долговременные соединения не удаляются в случае удаления
объекта-отправителя. Соединение восстанавливается, когда создается новый объект с
именем первоначального отправителя сигнала. Если удаляется
объект-приемник, то подключение сигнала удаляется в любом случае.

Приемник может создать долговременное подключение даже пока передатчик еще не
создан. Все анонимные подключения  DCOP должны быть долговременными.

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

   QByteArray params;
   QDataStream stream(params, IO_WriteOnly);
   stream << pid;
   kapp->dcopClient()->emitDCOPSignal("clientDied(pid_t)", params);

К этим сигналам подключается диспетчер задач панели KDE. Он использует анонимное
подключение (cигнал может быть отослан не только KLauncher), которое является
долговременным:

   connectDCOPSignal(0, 0, "clientDied(pid_t)", "clientDied(pid_t)", false);

Тем самым сигнал clientDied(pid_t) подключается к собственной DCOP функции
clientDied(pid_t). В этом случае и сигнал, и вызываемая функция имеют одинаковые
имена. В этом нет необходимости, если аргументы сигнала и функции совпадают.
Принимающая функция может игнорировать один или несколько аргументов в конце
списка сигнала. То есть допускается подключение сигнала  clientDied(pid_t) к
DCOP функции clientDied(void).

Использование компилятора dcopidl:
----------------------------------

dcopidl облегчает настройку сервера DCOP. Вместо написания метода process() и
распаковки параметров (из массива QByteArray) вручную, вы можете позволить
компилятору dcopidl создать за вас весь необходимый код.

Кроме того, вы можете описать интерфейс вашего класса в одном, отдельном файле
заголовка.

Написание файла IDL очень напоминает обычный файл заголовка C++. Исключение
составляет ключевое слово 'ASYNC'. Оно обозначает, что вызов этой функции будет
осуществляться асинхронно. Компилятора C++ это ключевое слово заменяет на 'void'.

Пример:

#ifndef MY_INTERFACE_H
#define MY_INTERFACE_H

#include <dcopobject.h>

class MyInterface : virtual public DCOPObject
{
  K_DCOP

  k_dcop:

    virtual ASYNC myAsynchronousMethod(QString someParameter) = 0;
    virtual QRect mySynchronousMethod() = 0;
};

#endif

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

Если вы используете стандартный скрипт сборки KDE, то вы можете просто добавить
этот файл (который можно назвать MyInterface.h) в каталог с вашими исходниками.
Затем необходимо отредактировать файл Makefile.am, добавив в список SOURCES
'MyInterface.skel' и MyInterface.h в include_HEADERS.

Этот скрипт использует dcopidl для анализа  MyInterface.h, преобразования его в
XML-описание в MyInterface.kidl. Затем автоматически создается файл
MyInterface_skel.cpp, компилируется и связывается с исполняемым файлом вашей
программы.

Кроме этого, вы должны выбрать, какой класс вашей программы будет реализацией
интерфейса, описанного в MyInterface.h. Измените наследование этого класса так,
чтобы он виртуально наследовался от MyInterface. Затем добавьте в класс
интерфейса объявления, аналогичные MyInterface.h, но без виртуальных методов (не
чисто виртуальные).

Пример:

class MyClass: public QObject, virtual public MyInterface
{
  Q_OBJECT

  public:
    MyClass();
    ~MyClass();

    ASYNC myAsynchronousMethod(QString someParameter);
    QRect mySynchronousMethod();
};

Внимание: (Особенность Qt) Запомните, что если ваш класс наследован от QObject,
то вы должны указать его первым в списке наследуемых классов.

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

Пример:

MyClass::MyClass()
  : QObject(),
    DCOPObject("MyInterface")
{
  // whatever...
}

Теперь вы можете просто реализовать метод, описанный в интерфейсе, практически
так, как вы это обычно делаете.

Пример:

void MyClass::myAsynchronousMethod(QString someParameter)
{
  qDebug("myAsyncMethod called with param `" + someParameter + "'");
}

При этом не обязательно (хотя желательно) определять интерфейс как абстрактный
класс, так как мы делали в примере выше. Мы могли бы только описать секцию
k_dcop непосредственно в классе MyClass:

class MyClass: public QObject, virtual public DCOPObject
{
  Q_OBJECT
  K_DCOP

  public:
    MyClass();
    ~MyClass();

  k_dcop:
    ASYNC myAsynchronousMethod(QString someParameter);
    QRect mySynchronousMethod();
};

Кроме структур (скелетов), dcopidl2cpp также создает так называемые заглушки.
Это позволяет вызывать интерфейс DCOP без ручной распаковки. Чтобы использовать
заглушки, необходимо добавить MyInterface.stub в список SOURCES файла Makefile.am.
Класс заглушек называется MyInterface_stub.

Заключение:

Мы надеемся, что этот документ облегчит вам путь в мир межпроцессных
взаимодействий KDE! Все комментарии и пожелания направляйте авторам:
Preston Brown <pbrown на kde.org> и Matthias Ettrich <ettrich на kde.org>.
Замечания по переводу можете направлять Ilya V. Yalovoy <i_yalovoy на mail.ru>
или в список рассылки русскоязычных пользователей KDE: kde-rus на kde.ru.


Взаимодействие между пользователями:
------------------------------------

Особый интерес может представлять возможность взаимодействия посредством DCOP
между процессами, запущенными от имени разных пользователей. Так,
например, взаимодействие между программой, запущенной от имени пользователя, и
реализующей интерфейс к некоему процессу, работающему в фоновом режиме с правами
root.

Для реализации такой работы необходимо обеспечить выполнение двух условий:

1. оба процесса должны быть подключены к одному серверу DCOP.
2. должна быть обеспечена аутентификация.

Чтобы обеспечить выполнение первого условия, необходимо передать адрес сервера
(из .DCOPserver) во второй процесс. Для аутентификации можно
использовать переменную окружения ICEAUTHORITY, чтобы сообщить второму процессу,
где находится информация для аутентификации. (Учтите, что в данном случае
подразумевается, что второй процесс имеет права доступа для
чтения файла аутентификации, другими словами, второй процесс должен работать с
правами root. Если он должен быть запущен от имени другого пользователя, то
должны быть использован такой же подход, как kdesu работает через xauth.
Хорошей идеей было бы добавить поддержку DCOP в kdesu!).

Пример

ICEAUTHORITY=~user/.ICEauthority kdesu root -c kcmroot -dcopserver `cat ~user/.DCOPserver`

kdesu получает пароль root, запускает kcmroot от имени root и подключается к
серверу DCOP.

ВНИМАНИЕ: Сообщения, передаваемые с помощью DCOP, не шифруются, поэтому не
рекомендуется передавать этим путем важную и конфиденциальную информацию.

Тест производительности:
------------------------

Несколько тестов "прямо на салфеткеЭ:

Исходный код:

#include <kapplication.h>

int main(int argc, char **argv)
{
  KApplication *app;

  app = new KApplication(argc, argv, "testit");
  return app->exec();
}

Параметры компилятора:

g++ -O2 -o testit testit.cpp -I$QTDIR/include -L$QTDIR/lib -lkdecore

при использовании Linux были получены следующие результаты по использованию
памяти:

VmSize:     8076 kB
VmLck:         0 kB
VmRSS:      4532 kB
VmData:      208 kB
VmStk:        20 kB
VmExe:         4 kB
VmLib:      6588 kB

Если создать KApplication's DCOPClient, вызвать attach() и registerAs(), то
получим:

VmSize:     8080 kB
VmLck:         0 kB
VmRSS:      4624 kB
VmData:      208 kB
VmStk:        20 kB
VmExe:         4 kB
VmLib:      6588 kB

В общем, видно, что использование DCOP дополнительно расходует 100К памяти
для исполнения, но не расходует памяти для данных или стека. Учитывая, что
этот расход общий для всех приложений, то результаты вовсе не плохие!

Приведем еще несколько тестов. Запуск и немедленный выход KApplication
(т.е. убираем вызов KApplication::exec) занимает следующее время:

0.28user 0.02system 0:00.32elapsed 92%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (1084major+62minor)pagefaults 0swaps

То есть около 1/3 секунды на моем PII-233. Теперь, создадим объект DCOP и
подключимся к серверу:

0.27user 0.03system 0:00.34elapsed 87%CPU (0avgtext+0avgdata 0maxresident)k
0inputs+0outputs (1107major+65minor)pagefaults 0swaps

Результат практически аналогичен предыдущему. Собственно время на создание
DCOPClient не выходит за пределы в вариациях времени выполнения, связанных с
работой системы ("шумы"). При запуске обеих программ я получил разброс времен
от .32 до .48.

========================================================================


Подробная информация о списке рассылки kde-russian