Статья
Версия для печати
Обсудить на форуме
часть 3: использование драйвера  (вариации на тему ICQ).


Написание клиентской части.

Первым побуждением при написании общего проекта было создание единого проекта-приложения (в терминологии PhAB - Photon Application Builder), первая часть которого (инициализация) создавала бы менеджер ресурса, регистрирующий уникальное имя (в проекте /proc/talk), а вторая - реализовывала бы пользовательский интерфейс к аналогичным экземплярам менеджеров ресурсов, запущенных на других хостах сети.

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

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

Отсюда следует, что рассматриваемый проект распадается на два процесса: процесс rmta, регистрирующий имя /proc/talk, и клиентский процесс clitalk, обеспечивающий пользовательские обращения к этому имени. Как оказалось, этот подход оказался плодотворнее позже также исходя из других соображений (в смысле потенциальной простоты развития такого проекта).

Полученный в результате итоговый проект, помимо использования техники написания менеджера ресурса (но, собственно, благодаря ей и архитектуре QNX), демонстрирует ещё две очень интересных особенности:
  • Принципиально сетевое приложение clitalk не содержит в своём коде ни единого сетевого оператора, всю рутинную работу транспортировки сообщений по сети берёт на себя QNET.
  • Неограниченно многопоточное приложение с клонированием экземпляров (сеансов связи, которые вы можете открывать в неограниченном количестве, это уже "заслуга" приложения rmta) не содержит в своём коде ни единого оператора явного порождения потока (по крайней мере, в той части кода, которая ответственна за клонирование экземпляров). Всё распараллеливание неявно обеспечивается системой передачи сообщений OS QNX и, отчасти, её графической подсистемой Photon.



На рис.5 показано построенное в PhAB (Photon Aplication Builder) изображение, и именования элементов клиентского приложения. Это приложение позволяет пользователю выбирать сетевые хосты (из списка доступных в сети) и порождать новые соединения (сеансы).

Кратко, в мере, необходимой для понимания исходного кода, рассмотрим порядок совместной работы приложений (и подготовленного ранее менеджера ресурса, и рассматриваемого клиентского приложения), полный текст работающего PhAB проекта может быть взят на myrm.tgz :

  • Запускаем на различных хостах сети приложения менеджера ресурса (исполнимый файл rmta). Для целей тестирования или отладки менеджер ресурса может быть запущен и на единственном локальном хосте (вырожденный случай), но при этом оба конца "канала" будут привязаны к единому компьютеру. Если на компьютере уже выполняется rmta, то вторая копия не может быть запущена (функция OnStartRM() в файле Phone.cpp).
  • Запускаем (на любом из хостов, на котором уже выполняется процесс менеджера ресурса) клиентскую программу (исполнимый файл clitalk).

Код: (C++)
// Фактический старт приложеня - выполняется по открытию главного окна
int OnBaseWindow( PtWidget_t *widget, ApInfo_t *apinfo,
                  PtCallbackInfo_t *cbinfo )    {
   char sTitle[ 80 ];  
   strcpy( sTitle, "QNET talk-connector, " );
   strcat( sTitle, sVers );
   PtSetResource( ABW_base, Pt_ARG_WINDOW_TITLE, sTitle, 0 );        
   // создание списка QNET-хостов
   if( CreateNetList() <= 0 ) FatalExit( "QNET not found" );
   PtListSelectPos( ABW_base_host, 1 );
   ShowSelectedHost( 1 );
   return( Pt_CONTINUE );
};

  • В окне base_host отображается список хостов, реально доступных в сети QNET (список обновляется по таймеру, функция callback OnTick() файла Initial.cpp, т.е. отдельные имена в списке могут динамически появляться и исчезать).

Код: (C++)
// заполнение списка (панели) доступных QNET-хостов
// ( имён хостов в локальном каталоге /net/... ),
// возвращаемое значение - число хостов,
// видимых в QNET сети ( <=0 - недоступно! ).
static int CreateNetList( void ) {
   DIR *dir;
   struct dirent *dent;
   char* newname = new char[ NAME_MAX + 1 ];  
   int n = 0;  
   PtListDeleteAllItems( ABW_base_host );  
   if( ( dir = opendir( "/net" ) ) == NULL ) return( -1 );
   for( n = 0; dent = readdir( dir ); n++ ) {
       sprintf( newname, "%s", dent->d_name );        
       PtListAddItems( ABW_base_host, (const char**)&newname, 1, 0 );        
   };
   delete newname;
   closedir( dir );
   return n;
};
//--------------------------------------------------------------------------
// имя выбранного хоста:
static char *sHost = new char[ PATH_MAX + 1 ];
//--------------------------------------------------------------------------
// копирование имени выбранного хоста в отдельную позицию на экране
static void ShowSelectedHost( int n ) {
   PtArg_t args[ 1 ];
   short *num;  
   char **items = NULL;
   PtSetArg( &args[ 0 ], Pt_ARG_ITEMS, &items, &num );
   PtGetResources( ABW_base_host, 1, args );
   strcpy( sHost, items[ --n ] );
   PtSetResource( ABW_base_select, Pt_ARG_TEXT_STRING, sHost, 0 );  
};

  • Текущий выбранный хост из списка сносится в поле base_select.
  • При нажатии "Связать" (base_connect) создаётся новый экземпляр объекта класса Line (по цепочке: функция OnConnect() файла Phone.cpp -> функция CreateLine() файла Line.cpp -> конструктор объекта Line). Каждый новый объект Line создаётся анонимным: после создания к нему нет доступа из программного кода (как это описано в [2]).

Код: (C++)
// создание канала к выбранному хосту
int OnConnect( PtWidget_t *link_instance, ApInfo_t *apinfo,
               PtCallbackInfo_t *cbinfo ) {
   if( !CreateLine( sHost ) )
      FatalExit( "Невозможно установить соединение", false );
   return( Pt_CONTINUE );
};

Конструктор Line запрашивает по open() двух менеджеров ресурсов (локальный и удалённый), которые создают для него 2 диалоговых окна обмена (возможно, на разных компьютерах). Получив от менеджеров ресурсов два файловых дескриптора (в ответ на open()), конструктор Line запускает отдельный поток (функция Exchanger() файла Line.cpp) взаимного обмена (синхронизации) окон c интервалом в 1 сек.
Код: (C++)
// Обновление списка доступных хостов по таймеру
int OnTick( PtWidget_t *widget, ApInfo_t *apinfo,
            PtCallbackInfo_t *cbinfo ) {
   CreateNetList();
   int n = PtListItemPos( ABW_base_host, sHost  );
   if( n == 0 ) n = 1;
   PtListSelectPos( ABW_base_host, n );    
   ShowSelectedHost( n );    
   return( Pt_CONTINUE );
};
//--------------------------------------------------------------------------
// фиксация и отображение выбранного в диалоге хоста
int OnSelectHost( PtWidget_t *widget, ApInfo_t *apinfo,
                  PtCallbackInfo_t *cbinfo ) {
   ShowSelectedHost( ( (PtListCallback_t*)cbinfo->cbdata )->item_pos );    
   return( Pt_CONTINUE );
};

Клиент при этом выполняет только группу POSIX операций read() -> write().

  • Далее клиент уже фактически не выполняет ничего (он вообще на протяжении всей работы не подозревает о каких либо окнах), кроме 2-х встречных потоков копирования между дескрипторами, открытыми по open() ранее. Вот как реализовано (в основном) тело объекта Line, обеспечивающего коммуникационный канал:

Код: (C++)
static void* Exchanger( void* );
//--------------------------------------------------------------------------
class Line {
private:
    static const int nbuf = 1024;
    int df, dt, hdf, hdt, tid;
public:
    Line( char* );    
    ~Line( void );
    bool OK( void ) { return ( ( hdf > 0 ) && ( hdt > 0 ) ); };
    int Dscr( bool bDir ) { return bDir ? hdf : hdt; };
    bool Exchange( void );
};
//--------------------------------------------------------------------------
// Поток обновления (инициирующий передачу по каналу)
static void* Exchanger( void* p ) {
   Line *L = (Line*)p;
   while( true ) {
      if( ! L->Exchange() ) break;
      sleep( 1 ); // выдержка интервала взаимо обмена
   };
   delete L;
   return NULL;
};
//--------------------------------------------------------------------------
Line::Line( char *sH ) {
   char sRem[ PATH_MAX + NAME_MAX + 1 ];  
   strcpy( sRem, sResName );  
   int df = open( sRem,  O_RDWR );   hdf = df;
   strcpy( sRem, "/net/" );
   strcat( sRem, sH );  
   strcat( sRem, "/" );        
   strcat( sRem, sResName );  
   int dt = open( sRem,  O_RDWR );   hdt = dt;  
   tid = -1;
   int ph[ 2 ] = { hdf, hdt };  
   tid = ThreadCreate( 0, &Exchanger, (void*)this, NULL );      
};
//--------------------------------------------------------------------------
Line::~Line( void ) {
   int h;
   close( h = hdf );
   close( h = hdt );
};    
//--------------------------------------------------------------------------
bool Line::Exchange( void ) {
   int h, n;      
   char buf[ nbuf ];
   n = read( h = hdf, buf, sizeof( buf ) );
   if( strcmp( buf, sEof ) == 0 ) return false;
   if( strlen( buf ) != 0 ) write( h = hdt, buf, strlen( buf ) + 1 );      
   n = read( h = hdt, buf, sizeof( buf ) );
   if( strcmp( buf, sEof ) == 0 ) return false;
   if( strlen( buf ) != 0 ) write( h = hdf, buf, strlen( buf ) + 1 );    
   return true;
};
//--------------------------------------------------------------------------
// Создание новой линии связи от локального хоста к хосту sHost
bool CreateLine( char* sHost ) {
   Line* pL = new Line( sHost );
   if( !pL->OK() ) delete pL;
   return pL->OK();
};

Обратите внимание, что "внаружу" этого файла реализации видна только одна функция - CreateLine, которая всего лишь и объявлена в Line.h, и используется клиентом для создания коммуникационного канала.

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

Примечания к исходным кодам: в исходных кодах есть 2 места, которые могут внести невнятицу в картину происходящего. Они оставлены ввиду своей малой значимости и недостатка времени (AS IS). Ниже они объясняются детально:
  • В операциях чтения / записи класса Line стоят "странные" операторы вида:

    n = read( h = hdf, buf, sizeof( buf ) );

    Это связано с тем, что после возврата из функций read() и write() значение файлового дескриптора портится (обнуляется). Этот эффект - недоработки показанного менеджера ресурса и манипуляции в нём с флагами. Чтобы не возиться с локализацией этой ошибки, вместо этого, файловый дескриптор просто присваивается промежуточной переменной перед операцией. Того же результата можно было добиться С++ трюком:

    n = read( ( hdf ), buf, sizeof( buf ) );
  • При закрытии пользователем окна phone (завершении сеанса), реакция на закрытие заблокирована, и МР при очередном read() возвратит условную строку конца файла (строка sEof файла Common.h). По получению этой строки только объект Line закрывает окно, выполняя close() над файловым дескриптором (хотя бы только потому, что окна нужно уничтожать в паре - те которые составляют сеанс). Корректнее было бы реализовать признак EOF операции read() согласно POSIX - возвратом 0. Но в данном проекте это не принципиально.

Выводы и направления развития.

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

Обладая ранее некоторым опытом создания подобных приложений для Win32 и Linux, я предполагаю, что "ручная" реализация приложения, аналогичного описанному, была бы в 5-8 раз объёмнее приведенной. (Правда, в приведенном тексте каждый оператор "плотнее" и критичнее к ошибкам).

Можно предложить и некоторые последующие улучшения предложенной схемы взаимодействия: в текущей реализации синхронизация содержимого окон сделана по таймеру. Это решение оставлено в какой то мере сознательно - при изучении работы проекта очень наглядно видеть моменты синхронизации. Более развитое решение должно предполагать асинхронную реализацию операции read() в менеджере ресурса (текущая реализация - синхронная, но в технике менеджеров ресурса ничто не препятствует реализации асинхронных операций). При этом из класса Line будет исключена необходимость отдельного потока синхронизации окон.

Источники дополнительной информации:
[1] - "Writing Resource Manager" в HELP подсистеме операционной системы QNX RTP 6.1.
[2] - Олег Цилюрик, "Визуальные инкапсулированные компоненты в Photon QNX", журнал "Программист", М.:, 5, 2002.
Версия для печати
Обсудить на форуме