&MCOP;: Потоки и объектные модели
Описание
&MCOP; используется в &arts; для:
Связи между объектами.
Прозрачности работы в сети.
Описания интерфейсов объектов.
Независимости от языка.
Важным составляющим &MCOP; является язык описания интерфейса &IDL;, с помощью которого описано большинство интерфейсов &arts; и API.
Чтобы использовать &IDL;, интерфейсы из C++ компилируются &IDL;-компилятором вместе с кодом C++. При реализации интерфейса вы наследуете от базового класса &IDL;, созданного компилятором. При использовании интерфейсов это можно сделать с помощью надстраивания функциональности. Таким образом, &MCOP; может пользоваться своим протоколом, даже если объект нелокален. Так вы получаете прозраночть работы в сети.
В этой главе описываются основные черты объектной модели, получаемой при использовании &MCOP;, сам протокол и его использование в C++ (связывание языков) и т.д.
Интерфейсы и &IDL;
Большинство сервисов &arts; (к примеру, модули и звуковой сервер) определены в терминах интерфейсов. А интерфейсы описаны в формате, не зависящем от языка: &IDL;.
Таким образом, многие детали вроде формата потоков медиаданных, прозрачность сети и зависимости от языка программирования можно скрыть в описании интерфейса. Инструмент &mcopidl; преобразовывает определение интерфейса для конкретного языка программирования (сейчас поддерживается только C++).
Инструмент генерирует каркасный класс с основными функциями. Ваши собственные классы будут от него наследовать.
&IDL; использующийся в &arts; похож на язык, использующийся в CORBA and DCOM.
Файлы &IDL; могут содержать:
Директивы #include для других файлов &IDL;.
Объявления перечисляемых типов и структур, как в C/C++.
Объявления интерфейсов.
Интерфейсы в &IDL; - это почти то же самое, что класс в C++ или структура в C, но с некоторыми ограничениями. Как и в C++, интерфейсы наследовать от других интерфейсов. В определение интерфейса можно включать: потоки, атрибуты и методы.
Потоки
Потоки определяют медиаданные, они являются важнейшими компонентами модуля. Формат потока:
[ async ] in|out [ multi ] тип stream имя [ , имя ] ;
Направленность потоков зависит от квалификатора (выход или вход). Аргумент типа определяет тип данных (один из перечисленных ниже типов атрибутов), однако поддерживаются ещё не все типы. Во многих модулях типом потока ставится аудио, это внутренний формат данных. Несколько потоков одного типа могут быть объявлены через запятую.
По умолчанию потоки синхронны, т.е. передача данных идёт постоянно и на определённой частоте, как PCM-аудио. Если вы установите параметр async, поток будет асинхронным, т.е. данные будут передаваться с перерывами. Примером асинхронных потоков могут служить &MIDI;-сообщения.
Ключевое слово multi, допустимое только для входных потоков, указывает на то, что поток может принимать переменное количество входов. Это удобно при создании таких устройств, как микшеры, которые могут принимать любое количество входных потоков.
Атрибуты
Атрибуты - это данные, ассоциирующиеся с объектом интерфейса. Они определяются как переменные-члены классов в C++ и могут быть одного из простейших типов данных: boolean, byte, long, string или float - а такжеструктурами, определёнными пользователем, перечисляемого типа (enum) или последовательностью с переменной длиной (используется <type>). Лучше всего атрибуты помечать как доступные только для чтения.
Методы
Как и в C++, методы могут определяться в интерфейсах. Тип параметров метода может быть таким же, у атрибута. Ключевое слово oneway показывает, что метод возвращает какое-то значение сразу и выполняется асинхронно.
Стандартные интерфейсы
В &arts; уже определены несколько стандартных модульных интерфейсов, например, StereoEffect и SimpleSoundServer.
Пример
Простым примером модуля &arts; может служить модуль постоянных задержек из файла tdemultimedia/arts/modules/artsmodules.idl. Определение интерфейса приведено ниже.
interface Synth_CDELAY : SynthModule {
attribute float time;
in audio stream invalue;
out audio stream outvalue;
};
Модуль наследует от SynthModule. Этот интерфейс, описанный в artsflow.idl, определяет методы, использующиеся во всех модулях-синтезаторах.
Эффект CDELAY задерживает звуковой стереопоток на время, указанное как параметр с плавающей точкой. В определении интерфейса есть атрибут типа float для хранения длительности задержки. Он определяет два входных аудиопотока и два выходных. Никаких других методов, кроме тех, от которых он наследует, не требуется.
Подробнее о потоках
В этом разделе вы найдёте дополнительную информацию о потоках.
Типы потоков
Есть несколько вариантов реализации потоков в модуле. Вот несколько примеров:
Увеличение сигнала в два раза.
Выборочное изменение частоты.
Декодирование сигналов.
Чтение &MIDI;-событий из /dev/midi00 и добавление их в поток.
Первый случай очень прост: получив 200 сэмплов на входе, модуль воспроизводит 200 сэмплов на выходе. Т.е. выходные данные производятся только после получения входных.
Во втором случае при 200 входных сэмплов производится другое число выходных. Это зависит от выполненного преобразования, но их количество известно заранее.
В третьем случае всё ещё сложнее. Нельзя угадать заранее, сколько байтов будет сгенерировано из 200 сэмплов (возможно, гораздо больше, но...).
В последнем случае модуль активизируется сам по себе и иногда генерирует данные.
В &arts;-0.3.4 поддерживались потоки только первого типа, и большинство задач выполнялись. Возможно, это и требуется при написании модулей обработки аудиоданных. Со сложными типами потоков возникают проблемы, т.к. их очень сложно программировать и большая часть функций часто не нужна. Поэтому мы решили использовать два типа потоков: синхронные и асинхронные.
Характеристики синхронных потоков:
Модули должны обрабатывать входные данные любой длины (при условии, что их достаточно).
У всех потоков одна частота модуляции.
Функция calculateBlock() будет вызываться в том случае, если есть достаточное количество входных данных и указатели содержат ссылки на данные.
Резервирование и освобождение не выполняются.
Асинхронные потоки работают по-другому:
Модули могут формировать данные время от времени, с меняющейся частотой модуляции или только если они не ограничены правилом на запрос любой длины нужно отвечать
.
В асинхронных потоках частоты модуляции могут быть совершенно разными.
Исходящие потоки: содержат открытые функции для размещения пакетов, пересылки и учёта данных (используя этот механизм, вы будете знать, когда следует передать очередную порцию данных).
Входящие потоки: вызов происходит при получении нового пакета, вам нужно послать ответ, когда он будет обработан (вы можете послать сообщение об этом позже, если пакет кем-нибудь обработан, он будет освобождён/использовано заново).
В определении потоков используется ключевое слово async
для указания асинхронного потока. Если вы, к примеру, решили преобразовать ваш асинхронный поток байтов в синхронный поток сэмплов, интерфейс должен выглядеть так:
interface ByteStreamToAudio : SynthModule {
async in byte stream indata; // the asynchronous input sample stream
out audio stream left,right; // the synchronous output sample streams
};
Использование асинхронных потоков
Предположим, вам нужно написать модуль, воспроизводящий звук асинхронно. Его интерфейс будет выглядеть следущим образом:
interface SomeModule : SynthModule
{
async out byte stream outdata;
};
Как посылать данные? Первый способ называется принудительная доставка
. В асинхронных потоках данные посылаются пакетами. Это значит, что вы посылаете отдельные пакеты байтов, как в примере выше. Вся процедура состоит в том, чтобы разместить пакет, заполнить его и послать.
Вот пример кода. Сначала пакет размещается:
DataPacket<mcopbyte> *packet = outdata.allocPacket(100);
Потом он заполняется:
//для fgets необходим указатель (char *)
char *data = (char *)packet->contents;
//как видите, размер пакета можно уменьшить после размещения
if(fgets(data,100,stdin))
packet->size = strlen(data);
else
packet->size = 0;
И теперь посылаем:
packet->send();
Как видите, это достаточно просто. Но если пакеты нужно посылать с такой скоростью, чтобы получатель успевал их обрабатывать, нужен другой подход - доставка с задержкой
. Сначала вы посылаете какое-то количество пакетов, в то время, когда получатель по очереди их обрабатывает, формируете новые и опять посылаете их.
Вызов производится командой setPull. Например:
outdata.setPull(8, 1024);
Это значит, что вы хотите посылать пакеты через outdata и начать с 8, а когда получатель обработает несколько, восполнить их.
После этого нужно указать метод заполнения пакетов. Он может выглядеть так:
void request_outdata(DataPacket<mcopbyte> *packet)
{
packet->size = 1024; //не больше 1024
for(int i = 0;i < 1024; i++)
packet->contents[i] = (mcopbyte)'A';
packet->send();
}
Вот и всё. Когда закончатся пакеты, установите размер пакетов в ноль, что предотвратит их дальнейшую отправку.
Заметьте, что очень важно называть метод определённым образом: request_имя потока.
Мы обсудили, как отправлять данные. Получать их намного проще. Предположим, есть простой фильтр ToLower, который преобразовывает все буквы в нижний регистр:
interface ToLower {
async in byte stream indata;
async out byte stream outdata;
};
Очень простое использование:
class ToLower_impl : public ToLower_skel {
public:
void process_indata(DataPacket<mcopbyte> *inpacket)
{
DataPacket<mcopbyte> *outpacket = outdata.allocPacket(inpacket->size);
//преобразование в нижние регистр
char *instring = (char *)inpacket->contents;
char *outstring = (char *)outpacket->contents;
for(int i=0;i<inpacket->size;i++)
outstring[i] = tolower(instring[i]);
inpacket->processed();
outpacket->send();
}
};
REGISTER_IMPLEMENTATION(ToLower_impl);
И опять обратите внимание на имя метода process_имя потока.
Как видите, при получении пакета вызывается функция (в нашем случае это process_indata). А чтобы показать, что пакет обработан, нужно вызвать метод processed().
Совет по использованию: если обработка проходит медленно (к примеру, если нужно ждать вывода данных звуковой картой), не вызывайте processed() сразу же, а только после того, как пакет будет действительно обработан. Тогда отправитель будет знать, сколько времени требуется на самом деле.
Т.к. асинхронные потоки синхронизируются не очень хорошо, старайтесь использовать синхронные, а асинхронные только в крайнем случае.
Стандартные потоки
Предположим, есть 2 объекта, например, AudioProducer и AudioConsumer. У AudioProducer есть выходной поток, а у AudioConsumer - входной. Соединяя их, вы будете использовать эти потоки. С помощью стандартных потоков соединение упрощается: не нужно указывать порты.
Пусть теперь у нас есть объекты стререозвука, у каждого есть левый
и правый
порт. Очень хочется, чтобы подключение было как можно проще. Но как система узнает, какие порты соединять? Тут опять помогут стандартные потоки: можно указать несколько потоков по порядку. Поэтому, когда вы будете подключать два выходных стандартных потока к двум входным, не нужно будет указывать порты, а соответствие будет правильным.
Конечно, это не ограничено стреозвуком. Любое количество потоков можно сделать стандартным при необходимости, а функция связи будет проверять совпадение количества стандартных потоков двух объектов (в необходимом направлении), если вы не укажете порты.
Ключевое слово default в &IDL; может указывать в описании потока или в отдельной строке. Например:
interface TwoToOneMixer {
default in audio stream input1, input2;
out audio stream output;
};
В этом примере у объекта два входных порта будут соединены по умолчанию. Порядок определяется по строке со словом default. Поэтому у такого объекта:
interface DualNoiseGenerator {
out audio stream bzzt, couic;
default couic, bzzt;
};
Соединение couic
с input1
и bzzt
с input2
будет установлено автоматически. Заметьте, что в этом случае единственный выходной порт будет стандартным (смотрите ниже). Синтаксис генератора шума удобен для описания другого порядка или выбора только некоторых портов по умолчанию. Направления портов будет назначать &mcopidl;, поэтому не указывайте их. Входные и выходные порты можно записать в одной строке, важен лишь порядок.
Есть несколько правил наследования:
Если в &IDL; указан стандартный список, пользуйтесь им. В него могут быть добавлены родительские порты, независимо от того, были ли они стандартными.
Иначе наследоваться будут родительские порты по умолчанию в таком порядке: родитель1 порт1, родитель1 порт2, ..., родитель2 порт1, ... Если есть общий предок с двумя родительскими ветвями, по умолчанию использоваться будет первый попавшийся в списке порт.
Если порта по умолчанию нет, но есть одночный поток в каком-то направлении, используйте его как стандартый для этого направления.
Флаги смены атрибута
Флаги смены атрибута - это способ показать, что атрибут изменился. Они похожи на сигналы и функции внешнего вызова в &Qt; или Gtk. Например, если есть элемент &GUI; ползунок, отмечающий значение от 0 до 100, то должен быть объект, работающий с этим значением (к примеру, он может управлять громкостью сигнала). Будет удобно, если объект будет знать, изменился ли уровень громкости. Связь между отправителем и получателем.
В &MCOP; есть возможность контролировать изменения атрибутов. Независимо от того, что объявлено в &IDL;, атрибут
может (и должен) изменяться, а также получать сообщения об изменении. Например, если у вас было два &IDL;-интерфейса:
interface Slider {
attribute long min,max;
attribute long position;
};
interface VolumeControl : Arts::StereoEffect {
attribute long volume; // 0..100
};
Вы можете их связать с помощью флагов изменения. В этом случае связь будет выглядеть так (код на C++):
#include <connect.h>
using namespace Arts;
[...]
connect(slider,"position_changed",volumeControl,"volume");
Как видите, у каждого атрибута есть два разных потока: для отправки извещений об изменении вызывается имя атрибута _changed и для получения — attributename.
Важно знать, что флаги изменения совместимы с асинхронными потоками. А также они "прозрачны", поэтому вы можете связать атрибут типа float элемента &GUI; с асинхронным потоком модуля синтезатора на другом компьютере. Естественно, изменение флага не будет синхронным , т. к. на передачу уходит некоторое время.
Отправка извещений об изменении
Если вы используете объекты с атрибутами, извещение об изменении нужно посылать каждый раз, когда атрибут меняется. Код выглядит приблизительно так:
void KPoti_impl::value(float newValue)
{
if(newValue != _value)
{
_value = newValue;
value_changed(newValue); // <- послать извещение
}
}
Мы рекомендуем такой код для всех создаваемых объектов, чтобы флаги изменения могли использовать другие люди. Однако не стоит посылать извещения слишком часто, поэтому если вы обрабатываете сигнал, будет удобно записывать, когда было послано последнее извещение, чтобы не посылать его с каждым сэмплом.
Приложения для изменения флага
Будет особенно полезно менять флаг вместе с оболочками (которые, к примеру, визуализируют аудио данные), элементами gui, контроля и мониторинга. Такой код находится в tdelibs/arts/tests а экспериментальая реализация artsgui - в tdemultimedia/arts/gui.
Файл .mcoprc
Файл .mcoprc (в каталоге home каждого пользователя) может быть использован для настройки &MCOP;. Сейчас возможно следущее:
GlobalComm
Имя интерфейса для глобальной связи. Глобальная связь необходима для того, чтобы находить другие объекты и получать личные данные пользователя. Для разных &MCOP;-клиентов/серверов, которые должны быть как-то связаны, нужен общий объект GlobalComm для разделения информации между ними. Возможные значения: Arts::TmpGlobalComm
для связи посредством каталога /tmp/mcop-имя пользователя (который будет только на локальном компьютере) и Arts::X11GlobalComm
для связи через свойства корневого окна сервера X11.
TraderPath
Указывает, в каком каталоге хранится информация о трейдере. Вы можете перечислить несколько, разделив их запятой.
ExtensionPath
Указывает, из каких каталогов загружаются расширения (в форме общих библиотек). Несколько значений разделяются запятой.
Вот пример использования:
# $HOME/.mcoprc file
GlobalComm=Arts::X11GlobalComm
#если вы разработчик, будет удобно добавлять путь к каталогу трейдера
#т.е. вы сможете использовать добавлять компоненты, не устанавливая их
TraderPath="/opt/kde2/lib/mcop","/home/joe/mcopdevel/mcop"
ExtensionPath="/opt/kde2/lib","/home/joe/mcopdevel/lib"
&MCOP; для пользователей CORBA
Если вы пользовались CORBA раньше, вы заметите, что &MCOP; очень похож на эту технологию. Вообще-то до версии 0.4 &arts; использовал CORBA.
Основная идея CORBA такая же: вы создаёте объекты (компоненты). В &MCOP; ваши объекты доступны как обычные классы, в том числе и для удалённого сервера. Для этого нужно определить интерфейс объектов в файле &IDL;, так же, как и в CORBA. Однако есть несколько различий.
Функции CORBA, которые отсутствуют в &MCOP;
В &MCOP; нет параметров вход
и выход
вызова методов. Параматры всегда входящие, а возвращаемый код всегда исходящий. Это значит, что интерфейс:
// CORBA idl
interface Account {
void deposit( in long amount );
void withdraw( in long amount );
long balance();
};
пишется как
// MCOP idl
interface Account {
void deposit( long amount );
void withdraw( long amount );
long balance();
};
в &MCOP;.
Нет обработки исключений. В &MCOP; есть другие способы для обхода ошибок.
Здесь нет типа union и typedef. Не знаю, большой ли это недостаток...
Нет поддержки передачи интерфейсов и обращения к объектам
Функции CORBA, отличающиеся в &MCOP;
В &MCOP; последовательности определяются так: последовательностьтип
. Нет необходимости писать typedef. Например, вместо
// CORBA idl
struct Line {
long x1,y1,x2,y2;
};
typedef sequence<Line> LineSeq;
interface Plotter {
void draw(in LineSeq lines);
};
вы напишете
// MCOP idl
struct Line {
long x1,y1,x2,y2;
};
interface Plotter {
void draw(sequence<Line> lines);
};
Функции &MCOP;, которых нет в CORBA
Вы можете объявить потоки, которые будут обрабатываться платформой &arts;. Объявление потоков похоже на объявление атрибутов. Например:
// MCOP idl
interface Synth_ADD : SynthModule {
in audio stream signal1,signal2;
out audio stream outvalue;
};
Это значит, что ваш объект будет принимать два входящих синхронных аудиопотока signal1 и signal2. "Синхронный" значит, что эти потоки обязательно будут выдавать определённое количество данных. Т. е. если вызывается ваш объект и ему передаётся 200 сэмплов (signal1 + signal2), он выдаст 200 сэмплов.
Связь &MCOP; с C++
Основные отличия от CORBA:
Для работы со строками используется класс C++ STL string. Если они хранятся в последовательности, они хранятся просто тпе
, т.е. они считаются простым типом. Поэтому им необходимо копирование.
long - обычный тип long (32 бита).
Последовательности используют класс C++ STL vector.
Все структуры созданы из класса &MCOP; Type и сгенерированы компилятором &IDL;. Если они хранятся в массиве, для того, чтобы избежать копирования, хранятся только ссылки.
Создание объектов &MCOP;
После компиляции их нужно извлечь из класса _skel. Например, если вы определили ваш интерфейс так:
// MCOP idl: hello.idl
interface Hello {
void hello(string s);
string concat(string s1, string s2);
long sum2(long a, long b);
};
Вы компилируете его, вызвав mcopidl hello.idl, при этом сгенерируется hello.cc и hello.h. Чтобы эти файлы использовать, нужно определить C++ класс, который будет наследовать каркас:
//заголовочный файл C++ - hello.h - включается ранее
class Hello_impl : virtual public Hello_skel {
public:
void hello(const string& s);
string concat(const string& s1, const string& s2);
long sum2(long a, long b);
};
И, наконец, можете пользоваться методами как в обычном C++
// файл, использующий C++
// как видите, строки передаются указателями
void Hello_impl::hello(const string& s)
{
printf("Hello '%s'!\n",s.c_str());
}
// а если это возвращаемое значение, всё как с "обычной" строкой
string Hello_impl::concat(const string& s1, const string& s2)
{
return s1+s2;
}
long Hello_impl::sum2(long a, long b)
{
return a+b;
}
После этого вы получите объекты, которые могут "общаться" с помощью &MCOP;. Теперь осталось их создать (это делается так же, как в C++):
Hello_impl server;
И как только вы добавите ссылку
string reference = server._toString();
printf("%s\n",reference.c_str());
и перейдете в цикл ожидания
Dispatcher::the()->run();
Люди смогут обращаться к нему
// этот код может содержаться где угодно
// (даже на другом компьютере с другой архитектурой)
Hello *h = Hello::_fromString([the object reference printed above]);
и вызывать методы:
if(h)
h->hello("test");
else
printf("Access failed?\n");
Безопасность в &MCOP;
Так как передача данных между серверами &MCOP; идёт по протоколу TCP, любой (если вы подключены к Интернету) может попробовать подключиться к сервисам &MCOP;. Поэтому рекомендуется использовать аутентификацию клиентов. В &MCOP; используется протокол md5-auth
В md5-auth отбор клиентов, которые могут подключиться, происходит так:
Предполагается, что любой может получить ваш секретный пароль.
При каждом подключении клиента проверяется, знает ли он секретный пароль, при этом пароль не пересылается по сети, чтобы любой, прослушивающий сеть, не мог его получить.
Чтобы назначить каждому клиенту свой секретный пароль, &MCOP; запишет его в каталоге mcop (/tmp/mcop-USER/secret-cookie). Конечно, вы можете его скопировать на другой компьютер. Однако в этом случае нужен безопасный способ копирования, например, scp (из ssh).
Шаги аутентификации:
[Сервер] генерирует новый (случайный) пароль R
[Сервер] посылает его клиенту
[Клиент] читает "секретный пароль" S из файла
[Клиент] с помощью алгоритма MD5 преобразует пароли R и S и формирует пароль M
[Клиент] посылает M серверу
[Сервер] проверяет, действительно ли преобразование R и S даёт пароль M, полученный от клиента. Если да, аутентификация прошла успешно.
Этот алгоритм обеспечивает безопасность при условии, что
Секретный и случайный пароли достаточно случайны
и
Алгоритм искажения MD5 не позволяет восстановить исходный текст
, т.е. секретный S и случайный R пароли (который все знают) из преобразованного пароля M.
Каждое новое подключение по протоколу &MCOP; начинается с аутентификации. Это выглядит так:
Сервер посылает сообщение ServerHello, в котором описываются все известные протоколы аутентификации.
Клиент посылает сообщение ClientHello с информацией об аутентификации.
Сервер посылает сообщение AuthAccept.
Чтобы понять, как действительно работает система безопасности, нужно посмотреть, как обрабатываются сообщения во время аутентификации:
Пока не завершится этап аутентификации, никаких других сообщений сервер принимать не будет. Например, если он ожидает сообщение ClientHello
, а получает mcopInvocation, связь разрывается.
Если клиент вообще не посылает допустимых сообщений &MCOP; во время аутентификации, связь разрывается.
Если клиент пытается послать слишком большое сообщение (4096 байтов во время аутентификации), оно обрезается до 0 байтов, и сервер считает, что сообщение не было послано. Это необходимо, чтобы неаутентифицированный пользователь не мог послать сообщение в 100 мегабайт и израсходовал всю память сервера.
Если клиент присылает искажённое сообщение ClientHello, соединение разрывается.
Кроме того, нужно указать время ожидания на случай, если клиент вообще ничего не посылает.
Описание потокола &MCOP;
Введение
Протокол очень похож на CORBA, но он был расширен, чтобы выполнять все необходимые операции в реальном времени.
Вы можете создать объектную мультимедиа-модель, которую можно будет использовать для связи между компонентами в одном адресном пространстве (в одной задаче), а также между компонентами в различных потоках, задачах и на разных узлах.
Он будет доработан, чтобы выполнение проходило очень быстро и подходило "общительным" приложениям. Например, потоки видео - одно из приложений &MCOP;, где большая часть реализаций CORBA явно проигрывает.
Определения интерфейсов полностью реализуют следующую функуиональность:
Непрерывные потоки данных (например, аудиоданные).
Потоки событий (например, &MIDI;-события).
Счётчик ссылок.
и наиболее важные особенности CORBA , например:
Вызовы синхронных методов.
Вызовы асинхронных методов.
Создание определённых пользователем типов.
Множественное наследование.
Передача ссылок на объекты.
Упаковка сообщений &MCOP;
Идеи/цели дизайна:
Упаковка объектов должна быть проста в использовании.
При распаковке получатель должен знать, сообщение какого типа он собирается распаковывать.
Получатель должен использовать всю информацию, поэтому пропуски информации обычно бывают только в таких случаях:
Если вы знаете, что получите блок байтов, вам не нужно проверять каждый из них на наличие маркера конца передачи.
Если вы собираетесь получить строку, не нужно читать её до нулевого байта, чтобы вычислить длину, но
При получении массива строк нужно отслеживать длину каждой строки, чтобы узнать, когда закончится массив. Хотя если вы используете строки для чего-то важного, это нужно делать в любом случае.
Снижение непроизводительных издержек.
Упаковка сообщений различных типов показана в таблице ниже:
Тип
Процедура упаковки
Результат
void
в поток ничего не записывается
long
занимает четыре байта, и самый важный из них - первый; например, число 10001025 (0x989a81) будет упаковано так:
0x00 0x98 0x9a 0x81
enum
см. long
byte
занимает один байт; например, число 0x42 будет упаковано так:
0x42
string
см. long; отличия: содержит длину строки и последовательность символов, которая обязательно оканчивается нулевым байтом (он включён в длину)
длина строки должна включать последний нулевой байт!
например, hello
упаковывается так:
0x00 0x00 0x00 0x06 0x68 0x65 0x6c 0x6c 0x6f 0x00
boolean
см. байт; содержит 0, если false, и 1 если true; значение true выглядит так:
0x01
float
упаковывается в соответствии с четырёхбайтовым представлением IEEE754 -подробнее об этом можно узнать здесь: http://twister.ou.edu/workshop.docs/common-tools/numerical_comp_guide/ncg_math.doc.html и здесь: http://java.sun.com/docs/books/vmspec/2nd-edition/html/Overview.doc.html; например, значение 2.15 будет выглядеть так:
0x9a 0x99 0x09 0x40
struct
используется содержимое структуры; для этого не нужны дополнительные префиксы или суффиксы; например, структура
struct test {
string name; // это "hello"
long value; // это 10001025 (0x989a81)
};
будет упакована так:
0x00 0x00 0x00 0x06 0x68 0x65 0x6c 0x6c
0x6f 0x00 0x00 0x98 0x9a 0x81
последовательность
последовательность располагается так: количество элементов и сами элементы друг за другом
поэтому последовательность а из 3 элементов типа long, с a[0] = 0x12345678, a[1] = 0x01 и [2] = 0x42 будет упакована так:
0x00 0x00 0x00 0x03 0x12 0x34 0x56 0x78
0x00 0x00 0x00 0x01 0x00 0x00 0x00 0x42
При обращении к простому типу используется его имя. У структур и перечисляемых типов есть собственные имена. Последовательности являются указателями на *обычные типы, поэтому обращение к последовательности элементов типа long будет таким: *long
, а к последовательности структур Header: *Header
.
Сообщения
Формат заголовка сообщения &MCOP; определяется такой структурой:
struct Header {
long magic; // значение 0x4d434f50: MCOP
long messageLength;
long messageType;
};
Возможные типы сообщений:
mcopServerHello = 1
mcopClientHello = 2
mcopAuthAccept = 3
mcopInvocation = 4
mcopReturn = 5
mcopOnewayInvocation = 6
Несколько замечаний о сообщениях &MCOP;:
Каждое сообщение начинается с заголовка.
Некоторые сообщения должны отбрасываться сервером, если процесс аутентификации ещё не пройден.
После получения заголовка, можно получать всё сообщение целиком без просмотра его содержимого.
Значение messageLength в заголовке в некоторых случаях бывает лишним.
Однако это простой (и быстрый) способ обработки неблокового сообщения. С его помощью сообщения могут быть получены в фоновом режиме. Если одновременно установлено несколько соединений, они будут параллельными. Вам не нужно будет проверять содержимое всего сообщения (чтобы узнать, когда оно закончится), нужен только заголовок. Это упрощает кодирование.
После получения сообщения оно может быть распаковано и обработано за один проход, без рассматривания случаев, когда получены не все данные (это гарантирует messageLength).
Вызовы
Для вызова удалённого метода нужно послать структуру, приведённую ниже, в теле сообщения &MCOP; с messageType = 1 (mcopInvocation):
struct Invocation {
long objectID;
long methodID;
long requestID;
};
после этого параметры передаются как структура, т.е. если вы вызываете string concat(string s1, string s2), отправьте такую структуру:
struct InvocationBody {
string s1;
string s2;
};
Если нужен метод однонаправленный - т.е. асинхронный без возвращения значения - это был он. Иначе вы получите сообщение с messageType = 2 (mcopReturn)
struct ReturnCode {
long requestID;
<resulttype> result;
};
где <resulttype> - это тип результата. Вы можете написать только requestID, если тип метода был void.
Таким образом, concat(string s1, string s2) вернёт код
struct ReturnCode {
long requestID;
string result;
};
Изучение интерфейсов
Чтобы вызвать объект, нужно знать, какие методы можно использовать во время работы с ним. Значения 0, 1, 2, и 3 methodID определены для конкретных задач:
long _lookupMethod(MethodDef methodDef); // methodID всегда 0
string _interfaceName(); // methodID всегда 1
InterfaceDef _queryInterface(string name); // methodID всегда 2
TypeDef _queryType(string name); // methodID всегда 3
чтобы это прочесть, необходима сруктура
struct MethodDef {
string methodName;
string type;
long flags; // установить в 0 (необходимо для потоков)
sequence<ParamDef> signature;
};
struct ParamDef {
string name;
long typeCode;
};
в полях параметров содержатся компоненты, определяющие типы параметров. Тип возвращаемого значения зависит от типа MethodDef.
Строго говоря, только методы _lookupMethod() и _interfaceName() разные для всех объектов, а _queryInterface() и _queryType() всегда одинаковы.
Что же такое methodID? Если вы вызываете метод, нужно передавать его номер, т.к. цифры в запросе &MCOP; обрабатываются гораздо быстрее строк.
Итак, как же получить номер метода? Зная его подпись - MethodDef (в которой содержится имя, тип и информация о параметрах) - вы можете передать её в _lookupMethod объекта, вызывающего метод. Так как _lookupMethod настроен на methodID 0, проблем с этим не будет.
Если же вы не знаете подписи, с помощью _interfaceName, _queryInterface и _queryType можно узнать, какие методы поддерживаются.
Определения типов
Определённые пользователем типы данных описаны с помощью структуры TypeDef:
struct TypeComponent {
string type;
string name;
};
struct TypeDef {
string name;
sequence<TypeComponent> contents;
};
Почему &arts; не использует &DCOP;
В связи с тем, что в &kde; отказались от CORBA полностью и используют вместо него &DCOP;, обычно возникает вопрос, почему это не делается в &arts;. Тем не менее в TDEApplication есть хорошая поддержка &DCOP; для интеграции с libICE.
Возможно, многие захотят спросить, зачем нужен &MCOP;, если есть &DCOP;, поэтому скажу сразу. Поймите меня правильно, я не хочу сказать, что &DCOP; - это плохо
. Я просто хочу сказать, что &DCOP; не подходит &arts;
(хотя это хорошее решение для других задач).
Во-первых, нужно понять, для чего был написан &DCOP;. Созданный за два дня на встрече &kde;-2, он должен был быть как можно более простым, легковесным
протоколом связи. При разработке были упущены все сколько-нибудь сложные вопросы, например, подробное описание, как упаковывать типы данных.
Хотя в &DCOP; не важны некоторые вещи (например, как нужно посылать строку, чтобы обеспечить прозрачность сети?) - они необходимы. Поэтому всё, чего не может делать &DCOP;, добавлено в &Qt;. В основном, это управление типами (с помощью оператора сериализации &Qt;).
&DCOP; замечательно работает, позволяя приложениям &kde; отправлять простые сообщения вроде открыть в окне http://www.kde.org
или данные о конфигурации изменились
. Однако в &arts; важно другое.
Идея заключается в том, что небольшие модули &arts; должны общаться с помощью таких структур данных, как события midi
иуказатели на позицию в песне
.
Это сложные типы данных, которые должны пересылаться различными объектами как потоки или параметры. В &MCOP; есть возможность определить сложные типы через простые (похожие на структуры или массивы в C++). В &DCOP; программист должен сам писать, например, классы и проверять, что они правильно сериализуются (к примеру, поддержка потокового оператора в &Qt;).
Но в этом случае они будут доступны только для C++, так как нельзя разработать язык, который будет распознавать все типы модулей (которые не будут самоописывающимися).
Почти та же проблема с интерфейсами. В объектах &DCOP; информация о связях, иерархии наследования и т. д. закрыта. И если вам нужно написать приложение, которое должно показывать, какие атрибуты есть у этого объекта
, у вас ничего не получится.
Матиас (Matthias) рассказал, что есть специальная функция functions
для каждого объекта, которая определяет, какие методы поддерживает объект. Она пропускает информацию об атрибутах (параметрах), потоках и наследовании.
Это серьёзно нарушает целостность таких приложений, как &arts-builder;. Но не следует забывать, что &DCOP; разрабатывался не как объектная модель (как &Qt; с moc и подобными), не как что-то вроде CORBA, а только для обеспечения связи между приложениями.
Отличие &MCOP; состоит в том, что он должен работать с потоками, которые являются основным способом сообщения между объектами. В CORBA-версии &arts; приходилось разделять объекты SynthModule
, которые создавали потоки, и интерфейс CORBA
.
Основную часть кода занимала реализация взаимодействия объектов SynthModule
и интерфейсов CORBA
, она должна быть органичной, но не была, т.к. в CORBA не было даже понятия "поток". Взгляните на этот код (что-то вроде simplesoundserver_impl.cc ). Он выглядит намного лучше! Потоки можно объявлять в интерфейсах модулей, а их использование выглядит естественно.
Этого нельзя отрицать. Одной из причин написания &MCOP;, была скорость. Вот несколько объяснений, почему &MCOP; будет работать быстрее &DCOP;.
Вызов в &MCOP; содержит заголовок из 6 чисел типа long:
magic MCOP
;
тип сообщения (вызов);
размер запроса в байтах;
идентификатор запроса;
идентификатор цели;
илентификатор метода.
После этого последуют параметры. Заметьте, что распаковка при этом очень быстра. Вы можете использовать стандартные функции для распаковки объекта или метода, что сводит сложность кодирования к минимуму.
Сравним этот вариант с &DCOP;. В нём будет по крайней мере:
строка целевого объекта вроде myCalculator
;
строка addNumber(int,int)
, указывающая метод;
информация о протоколе, добавленная libICE, а также другие дополнительные данные, которых я не знаю.
Распаковывать все это гораздо сложнее, ведь вам придется обрабатывать строки, искать функции и т. д.
В &DCOP; все запросы проходят через сервер (DCOPServer). Это значит, что синхронный вызов выглядит так:
Задача-клиент посылает вызов.
DCOPserver (посредник) получает его, решает, куда нужно отправить запрос, и отправляет его настоящему
серверу.
Этот сервер получает вызов, выполняет запрос и отправляет результат.
DCOPserver (посредник) получает результат и... посылает его клиенту.
Клиент декодирует ответ.
В &MCOP; тот же вызов выглядит по-другому:
Задача-клиент посылает вызов.
Этот сервер получает вызов, выполняет запрос и отправляет результат.
Клиент декодирует ответ.
Если и то, и другое было выполнено правильно, всё равно передача запроса через &MCOP; в два раза быстрее. И всё же есть причины выбрать&DCOP;: если запущено 20 приложений, все они связаны друг с другом, в &DCOP; нужно 20 соединения, а в &MCOP; 200. Однако в работе с мультимедиа это не распространено.
Я пробовал сравнивать &MCOP; и &DCOP;, делая вызов как сложение двух чисел, подправив testdcop. Но не смог получить точные результаты для &DCOP;. Метод вызывался в том же процессе, где был вызов &DCOP;, и я не знал, как избавиться от одного сообщения об отладке, пришлось перенаправить выход.
В тесте использовлись один объект и одна функция. С увеличением количества объектов и функций результаты &DCOP; ухудшаются, а у &MCOP; остаются прежними. Кроме того, задача dcopserver не была подключена к другим приложениям. Возможно, в этом случае, работа маршрутизатора была бы медленнее.
Наконец, полученный результат: чуть больше 2000 вызовов в секунду у &DCOP; и чуть больше 8000 вызовов в секунду у &MCOP;. В четыре раза. И я знаю, что это не предел &MCOP; (для сравнения: mico в CORBA совершает 1000-1500 вызовов в секунду).
Если вы хотите более точных данных, напишите небольшие приложения для сравнения с &DCOP; и пришлите их мне.
В CORBA была возможность использовать однажды вызванный объект как отдельный серверный процесс
или как библиотеку
. Можно было использовать один и тот же код, а CORBA уже сам решала, что делать. Насколько я знаю, в &DCOP; такое невозможно.
С другой стороны, в &MCOP; это обязательно должно быть. Поэтому вы можете прослушивать какой-то эффект в &artsd;, а при этом редактор звуковых файлов использует его в своем пространстве задачи.
Если &DCOP; - это способ передачи данных между приложениями, то &MCOP; - связь внутри приложений. Это особенно важно для потоков мультимедиа, т.к. можно запускать несколько объектов параллельно.
Хотя &MCOP; ещё этого не поддерживает, возможность помечать приоритет остаётся. Например, так: это событие &MIDI; намного важнее этого вызова
. Или так: должно быть получено вовремя
.
С другой стороны, в &MCOP; может быть интергрирована передача потоков, объединённая с QoS. Если это будет сделано, &MCOP; не будет работать медленнее, чем TCP, но будет проще в использовании.
Нет необходимости писать связующее ПО для мультимедиа в &Qt;, иначе оно станет &Qt;-зависимым.
Насколько я знаю, тип пересылаемых по &DCOP; данных не важен, поэтому &DCOP; может использоваться отдельно от &Qt;. Вот пример повседневного использования в &kde;: пользователи посылают типы QString, QRect, QPixmap, QCString, ... Они используют сериализацию &Qt;. Поэтому если кто-то решит включить поддержку &DCOP;, например, в GNOME, он не сможет использовать типы QString и др. и ему придётся эмулировать работу &Qt; с потоками или посылать строку, пиксельные изображения и типы rect, что, конечно, никуда не годится.
&arts; не привязан к &kde;, он может работать как с &Qt; и X11, так и без них, и даже без &Linux; (я знаю людей, у которых он нормально работает в распространённых коммерческих ОС).
Я считаю, что компоненты, написанные не для &GUI;, не должны от него зависеть, чтобы была возможность более широкого их распространения среди разработчиков.
Я понимаю, что использование двух протоколов IPC неудобно, однако переход на &DCOP; я не считаю выходом. При желании можно попытаться объединить два протокола. Можно даже научить &MCOP; говорить на IIOP, получится CORBA ORB ;).
Мы разговаривали с Матиасом Этрихом (Matthias Ettrich) о будущем двух протоколов и нашли множество путей их развития. Например, &MCOP; мог бы осуществлять передачу сообщений в &DCOP;, это бы сблизило протоколы.
Поэтому возможными решениями могли бы быть:
Создание шлюза &MCOP; - &DCOP;, который осуществлял бы взаимодействие этих протоколов. Если вы заинтересовались, спешу сообщить, что рабочий прототип уже существует.
Интеграция в &MCOP; всего, чего пользователи ожидают от &DCOP;. Попытаться работать только с ним. Кто-нибудь может добавить в &MCOP;сервер-посредник
:)
Сделать основой &DCOP; не libICE, а &MCOP; и постепенно их объединять.
А можно использовать протоколы по их предназначению (оно ведь разное у каждого) и не пытаться их объединить.