Namespaces

Variants
Actions

Please note that as of October 24, 2014, the Nokia Developer Wiki will no longer be accepting user contributions, including new entries, edits and comments, as we begin transitioning to our new home, in the Windows Phone Development Wiki. We plan to move over the majority of the existing entries over the next few weeks. Thanks for all your past and future contributions.

Fundamentals of Symbian C++/Client-Server Framework/ru

From Wiki
Jump to: navigation, search
Article Metadata

Статья
Перевод:
hamishwillee
Последнее редактирование: hamishwillee (08 Dec 2011)

В Symbian ОС множество сервисов, например, файловый сервер, реализованы на основе клиент-серверной инфраструктуры. Эта статья посвящена обзору этой немаловажной части ОС и рассчитана на пользователей, желающих понять ее архитектуру или реализовать свой собственный сервер.

В качестве примера сервера будет рассмотрена модифицированная версия Central Repository (CR). CR отвечает за хранение постоянных настроек приложений, таких как настройки сетевой точки доступа, например. Эти настройки могут быть доступны множеству приложений.

Связанные между собой настройки организованы в группы, которые называются репозиторием (repository). Например, настройки сети хранятся вместе в одном репозитории, а настройки локали — в другом.

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

Более подробную информацию о CR можно найти здесь.

Contents

Обзор

Существует три причины, по которым лучше реализовать сервис в качестве сервера, а не совместно используемой библиотеки:

  1. Управление доступом к совместно используемым ресурсам. Например:
    • Файловый сервер предоставляет доступ к файловой системе.
    • Оконный менеджер предоставляет доступ к экрану.
    • Телефонный сервер предоставляет доступ к модему.
  2. Реализация программы, которая работает в фоновом режиме и извещает клиентов о каком-либо событии. Например:
    • Сервис, который извещает клиентов о новом SMS сообщении, приходящим на телефон.
    • Сервис, который извещает клиентов о появлении нового периферийного устройства, например, наушников.
  3. Управление политикой безопасности. Хотя серверы не должны запускать для каждого клиента отдельный процесс, на практике почти всегда именно так и происходит. А так как граница действий потока — это одно из самых уязвимых мест политики безопасности, то сервер, запущенный в своем собственном потоке, может применять политику безопасности контроля доступа к ресурсу. Например:
    • Файловый сервер может определять правила доступа к файлам.
    • Телефонный сервер может управлять доступом к сети.
    • CR может осуществлять контроль доступа к переменным, которыми он управляет.

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

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

Клиентская библиотека

Для того чтобы скрыть детали передачи сообщений и передачи данных от кода приложения, разработчик сервера может реализовать клиентский код для формирования запросов и передачи их на сервер через ядро. Обычно это выносится в отдельную библиотеку. Например, пусть приложение mytest.exe является клиентом для файлового сервера Symbian (efile.exe). Это приложение получает доступ к файловой системе благодаря библиотеке (efsrv.dll), которая реализует клиентскую сторону файлового сервера. Это продемонстрировано на рисунке:

File server.png

Синхронные вызовы

Диаграмма, представленная ниже, показывает простую последовательность синхронного взаимодействия клиента и сервера:

Client serv synchronous call.png

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

Серверная сторона принимает запрос и преобразует его в корректный объект, который выполняет и завершает запрос.

Асинхронные вызовы

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

На диаграмме ниже представлена простая последовательность действий при взаимодействии клиента и сервера. В данном примере клиент запрашивает сервер об необходимости предупреждать его о наступлении какого-либо события. Для выполнения запроса используется активный объект. Активный объект передается как параметр, сервер сохраняет сообщение и на этом взаимодействие заканчивается до наступления события. Спустя некоторое время, в тот момент когда возникает событие, сервер оповещает всех своих клиентов о наступлении этого события и на клиентской стороне вызывается функция RunL() активного объекта:

Client serv asynchronous call.png

Запуск и остановка сервера

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

Некоторые сервера, называемые непостоянными (transient), запущены только тогда, когда к ним подключены клиенты. Когда последний клиент отключается, сервер останавливается. Другие серверы работают постоянно (permanent) в независимости от того подключены ли к нему клиенты или нет. Системные серверы запускаются в обязательном порядке для нормального функционирования операционной системы, если по какой-либо причине такой сервер завершает свою работу, операционная система перезагружается.

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

Клиент-серверные классы

Главные классы клиент-серверной инфраструктуры представлены ниже:

Client server classes.png

Классы окрашенные в черный цвет, наряду с перечислением TServerRequest, являются модифицированными главными классами, реализующими клиент-серверную часть компонента Symbian CR.

CR – это сервер, обеспечивающий хранение настроек, которые должны совместно использоваться множеством приложений.

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

Настройки могут быть целого или строкового типа, можно задать новое значение (SetInt(), SetString()) и либо получить отдельное значение (GetInt(), GetString()), либо множество значений, совпадающих с заданным критерием (FindL()). Пользователи могут также прослушивать изменения настроек (NotifyRequest()).

Классы клиентской стороны

TIpcArgs

TIpcArgs используется для преобразования аргументов и передачи их на сервер. Объект TIpcArgs составляет полезную нагрузку клиент-серверного запроса, и может содержать до четырех 32-битных аргументов вместе с информацией о их типе. Также возможно, что аргументов может и не быть вовсе для запросов, для которых данные не нужны.

RHandleBase

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

RSessionBase

Это основной класс для взаимодействия с сервером. Большинство его методов объявлены с помощью ключевого слова protected, поэтому не могут быть вызваны напрямую пользовательским классом, но могут лежат в основе реализаций функций подклассов этого класса.

Класс определяет методы создания новой сессии CreateSession и отправки сообщений на сервер SendReceive и Send.

Метод SendReceive перегружен как для выполнения синхронного взаимодействия, так и асинхронного.

В асинхронный метод необходимо передать параметр TRequestStatus&, в роли которого клиент обычно использует активный объект для выполнения запроса.

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

TRequestStatus s=KRequestPending;
 
TInt r=Exec::SessionSendSync(iHandle,aFunction,(TAny*)aArgs,&s);
 
if (r==KErrNone)
 
{
 
User::WaitForRequest(s);
 
r=s.Int();
 
}

Это означает, что синхронный метод блокирует поток до тех пор, пока сервер не завершит каким-либо образом запрос.

RsessionBase::Send() посылает сообщение на сервер, но не дожидается ответа (на практике эта функция используется редко).

В методы Send() и SendReceive() необходимо передать 32-битный идентификатор, определяющий запрос клиента, и объект TIpcArgs, содержащий переданные аргументы. Идентификатор передается из перечисления, которое известно как клиенту, так и серверу. Для CR перечисление называется TServerRequest.

Некоторые серверы позволяют, чтобы их сессии использовались совместно, поэтому указатель на сессию может быть передан из одного потока в другой. Для того чтобы сделать сессию совместно используемой между множеством потоков в одном процессе, необходимо вызвать метод ShareAuto(), если между процессами — ShareProtected().

RRepositorySession

Клиентским представлением сессии сервера в этом примере является класс RRepositorySessionR класс, наследованный от RSessionBase.

Этот класс содержит функции инициализации сессии: Connect() и Open(). Если сервер еще не запущен, то реализация функции Connect() запускает его. Также имеется статическая функция RSessionBase::CreateSession, которой передается имя сервера, к которому необходимо подключиться.

На самом деле обращение к серверу по имени — потенциальная угроза безопасности, так как один сервер может выдать себя за другой сервер, используя его имя. Поддельный сервер, запущенный до настоящего, будет вводить в заблуждение пользователей, которые будут подключаться к нему, а не к настоящему серверу. Эта проблема в Symbian решается с помощью специальной возможности ProtServ. Программам с такой возможностью можно доверять, а их имена начинаются с !. Все имена системных серверов начинаются с такого символа и другие программы не могут их имитировать.

Класс сессии также обрабатывает взаимодействия с сервером и предоставляет различные функции для своих клиентов. Например, класс RFs, который наследуется от RSessionBase, обеспечивает доступ к файловому серверу, а также предлагает набор функций для работы с файлами и директориями, например, RFs::Delete() или RFs::GetDir(), которые преобразуют запрос к SendReceive, какие-либо переданные параметры упаковывают в структуру TipcArgs, и, наконец, формируют корректный идентификатор для функции, которую будут вызывать.

Когда в проектировании не используются подсессии, этот класс обычно представляет собой интерфейс для сервера. В случае если используются подсессии, класс являет собой «фабрику». Это справедливо для CR, а также для файлового сервера и сервера сокетов.

RSubSessionBase

Это основной класс для управления клиентской стороной подсессиями.

Создание сессии связано с тратой сравнительно больших ресурсов, так как объекты необходимо создавать как в ядре, так и в потоке сервера. Подсессии — это механизм, который позволяет иметь одному клиенту несколько отдельных соединений к серверу без создания сессии на каждое соединение. Это довольно таки эффективно, так как множество подсессиий работает в рамках одной сессии.

Многие сервера не используют подсесии: хорошим же примером, использующих их, является файловый сервер, в котором RFile — это подсессия, располагающаяся на RFs соединении. Это означает, что клиент может открыть множество файлов в контексте единственной клиент-серверной сессии.

RRepositorySubSession

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

Основной интерфейс клиентской стороны к репозиторию определен в этом классе.

Классы серверной стороны

RMessagePtr2

RMessagePtr2 - это класс необходимый для управления сообщением, пришедшим от клиента. Он определяет следующие методы:

  • Client(), идентифицирующий поток клиента
  • Complete(), посылающий сигнал клиенту о завершении запроса
  • Read() и Write() для чтения данных из и записи данных в адресное пространство клиента.

RMessage2

Этот класс, наследованный от RMessagePtr2, энкапсулирует единственный запрос от клиента. Он определяет:

  • метод Function(), который возвращает код выполнившейся функции
  • методы для получения каждого из четырех 32-битных аргументов, которые могут быть переданы клиентом либо как целые, либо указатели на TAny*.

Представление аргументов целыми или указателями позволяет клиентам отправлять больше данных, чем может вместиться в 32 бита, устанавливая значение указателя на дескриптор в клиентском адресном пространстве. Сервер получает этот указатель и используя функцию ReadL(), определенную в RMessagePtr2, читает данные.

Серверные классы

Главный серверный класс, CServer2, наследуется от CActive. Сообщения от клиента приходят в форме RMessage2 объектов, и после их получения сервер вызывает метод RunL}. Метод RunL просматривает Function() сообщения и:

  • Если это сообщение подключения, то с помощью чистого виртуального метода NewSessionL создает новый подкласс CSession2.
  • Если это сообщение отключения, то уничтожает соответствующий подкласс CSession2.
  • Иначе, отправляет сообщение в метод ServiceL соответствующего CSession2.
EXPORT_C void CServer2::RunL()
{
 
TInt fn = Message().Function();
 
if(fn>=0)
{
// Сообщение
 
CSession2* session=Message().Session();
if(session)
session->ServiceL(Message());
else
NotConnected(Message());
 
}
else if(fn==RMessage2::EConnect)
{
Connect(Message());
}
else if(fn==RMessage2::EDisConnect)
{
Disconnect(Message());
}
else
{
BadMessage(Message());
}
// Прием следующего сообщения, если это уже не было сделано
 
if(!IsActive())
ReStart();
}

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

Автор сервера обязан реализовать подкласс CPolicyServer или CServer2, которые включают в себя функцию-фабрику NewSessionL.

Сервер может и не наследоваться от CPolicyServer, в том случае если не требуется применять политику безопасности, а прямо наследоваться от CServer2.

Также, если сервер требует более гибкой политики безопасности, чем может обеспечить CPolicyServer, он может наследоваться от CServer2 и реализовать свою собственную политику безопасности с нуля. Именно так и было сделано в CR.

Классы сессии

CSession2 — это класс серверной стороны, соответствующий RSessionBase. Для каждого инициализированного RSessionBase должен быть один класс CSession2. Ключевой функцией, объявленной в этом классе, является функция ServiceL.

Разработчик сервера обязан реализовать класс CSession2, которая включает в себя функцию ServiceL. Обычно ServiceL реализуется с помощью конструкции switch, которая проверяет идентификатор сообщения и выполняет соответствующие этому идентификатору действия.

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

Если сервер поддерживает подсессии, для каждого инициализированного подкласса RSubSessionBase клиентской стороны должен существовать класс серверной стороны, к которому будут направляться запросы. В случае с CR, этим классом является CServerSubSession, и запросы от клиента в конце концов попадают сюда для выполнения.

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

Передача данных

Если клиент и сервер запущены в отдельных процессах, данные никогда не могут быть переданы как обычные указатели С++, потому что сервер никогда не получит доступ к адресному пространству клиенту (и наоборот).

От клиента к серверу

Структура TIpcArgs, переданная от клиента к серверу, содержит четыре 32-битных значения. Если этого достаточно, то данные могут быть просто скопированы в сообщение и отправлены серверу. Иначе, всегда передается указатель на дескриптор в клиентском адресном пространстве, к которому сервер получает доступ через RMessagePtr2::Read() или RMessagePtr2::ReadL().

Если данные клиента представлены не в форме дескриптора, а, например, простой структурой без указателей, то они должны быть конвертированы в дескриптор классом TPckg.

Если данные содержат указатель, например, на С класс, они должны быть сериализованы в формат дескриптора. Обычно для этого используется метод ExternalizeL. Сервер прочитает дескриптор и пересоздаст объект с использованием соответствующей метода InternalizeL.

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

От сервера к клиенту

Клиент никогда не сможет читать данные из адресного пространства сервера, поэтому для того чтобы передать данные от сервер к клиенту, клиент должен передать в сообщении серверу указатель на дескриптор, в который он запишет данные с помощью метода RMessagePtr2::WriteL().

Примеры

Инициализация сессии

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

Клиентская сторона

Метод RRepositorySession::Connect() создает объект основного класса CreateSession(), передавая ему в коструктор имя сервера. Если сервер не найден, то метод пытается запустить его с помощью функции StartServer() несколько раз. Количество попыток ограничено:

TInt RRepositorySession::Connect()
 
{
 
const TVersion KVersion(KServerMajorVersion, KServerMinorVersion, KServerBuildVersion);
 
TInt retry = 2;
 
TInt err = KErrGeneral;
 
TInt numMessageSlots = -1;
 
for(;;)
 
{
 
// Попытка создать новую сессию с сервером.
err = CreateSession(KServerName, KVersion, numMessageSlots);
 
if((err != KErrNotFound) && (err != KErrServerTerminated))
break; // все нормально, выход из цикла
 
// Сервер не запущен — пытаемся сделать это.
 
if(--retry==0)
break; // Не удалось за ограниченное количество раз что-либо сделать.
 
err = StartServer();
 
if((err != KErrNone) && (err != KErrAlreadyExists))
break; // Сервер запустили
 
}
 
return err;
}

Серверная сторона

В случае с сервером вызывается функция RunL(), которая проверяет пришедшее сообщение и вызывает метод Connect(), который, наконец, вызывает метод NewSessionL(), создающий новый объект CServerSession.

Наконец, сервер завершает запрос клиента.

Иинициализация подсессии

Сейчас клиент может открыть определенный репозиторий, вызвав RRepositorySubsession::Open() и передав туда UID этого репозитория.

Клиентская сторона

RRepositorySubsession::Open() вызывает метод основного класса CreateSubSession():

TInt RRepositorySubSession::Open(RRepositorySession* aSession, TUid aUid)
{
return(CreateSubSession(*aSession, EInitialize, TIpcArgs(aUid.iUid));
}

Server Side

В случае с сервером вызывается функция RunL(), которая проверяет пришедшее сообщение и перенаправляет его корректному объекту сессии, которая открывает файл репозитория и создает объект подсессии для управления запросом клиента.

Наконец, сервер завершает запрос клиента.

Простое получение данных синхронным запросом - GetInt

Клиентская сторона

RRepositorySubSession::GetInt() в качестве параметра принимает идентификатор (UID) настройки, а сервер возвращает ее значение. Реализация на стороне клиента создает объект TPckg для преобразования целого типа и передает в метод класса родителя следующее:

  • Идентификатор для получения целого значения.
  • Объект TIpcArgs, содержащий UID настройки и указатель на TPckg объект.
TInt RRepositorySubSession::GetInt(TUint32 aId, TInt& aVal)
{
TPckg<TInt> p(aVal);
return SendReceive(EGetInt, TIpcArgs(aId, &p));
}

Серверная сторона

Главный серверный класс CSessionManager, перенаправляет сообщение корректному объекту сессии, который в свою очередь перенаправляет это сообщение в корректную подсессию. Подсессия просматривает идентификатор сообщения и вызывает внутренний метод GetIntL().

GetIntL() читает UID настройки (первый аргумент сообщения), проверяет права доступа клиента к настройке и, если проверка прошла успешно, читает значение настройки и пишет его в дескриптор, переданный вторым параметром:

TInt CServerSubSession::GetIntL(RMessage2 & aMessage)
{
 
TUint32 key = aMessage.Int0();
 
if(KErrNone != CheckPolicy(aMessage,iRepository.GetReadAccessPolicy(key))
return KErrPermissionDenied;
 
TInt val;
 
TInt error = iRepository.Get(key, val);
 
if (error == KErrNone)
{
TPckg<TInt> p(val);
aMessage.WriteL(1, p);
}
 
return error;
 
}

Наконец, сервер завершает запрос клиента.

Сложный поиск — FindL

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

Во-первых, клиентская сторона метода FindL создает массив для KDefaultSettingsCount настроек и передает его наряду с шаблоном в запросе.

Сервер перенаправляет сообщение в корректную подсессию, которая извлекает набор настроек и записывает их в переданный массив по следующему принципу:

  • В первый элемент массива записывается общее количество найденных настроек. Получается минимальный размер массива, который должен быть.
  • Последующие элементы заполняются значениями настроек, количество который будет KDefaultSettingsCount-1.


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

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

Асинхронное предупреждение — NotifyRequest

CR позволяет прослушивать изменение заданной настройки.

Клиентская сторона

В функцию RRepositorySubSession::NotifyRequest() передается идентификатор необходимой нам настройки и TRequestStatus, которым обычно становится активный объек:

void RRepositorySubSession::NotifyRequest(TUint32 aId, TRequestStatus& aStatus)
{
SendReceive(ENotifyRequest, TIpcArgs(aId), aStatus);
}

Серверная сторона

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

TInt CServerSubSession::NotifyRequest(RMessage2& aMessage)
{
TUint32 key = aMessage.Int0();
 
if(KErrNone != CheckPolicy(aMessage,iRepository.GetReadAccessPolicy(key))
return KErrPermissionDenied;
 
TInt error = iNotifier.AddRequest(key, aMessage);
 
return error;
}

Когда настройка меняется, обработчик события проходит по всему списку и для каждого сообщения, для которого совпадает изменившаяся настройка, вызывает RMessage2::Complete().

Это приводит клиентский TRequestStatus к завершению, за которым следует вызов функции активного объекта RunL().



Licence icon cc-by-sa 3.0-88x31.png© 2010 Symbian Foundation Limited. This document is licensed under the Creative Commons Attribution-Share Alike 2.0 license. See http://creativecommons.org/licenses/by-sa/2.0/legalcode for the full terms of the license.
Note that this content was originally hosted on the Symbian Foundation developer wiki.

This page was last modified on 8 December 2011, at 23:58.
293 page views in the last 30 days.

Was this page helpful?

Your feedback about this content is important. Let us know what you think.

 

Thank you!

We appreciate your feedback.

×