© Copyright 2006 Грибов Игорь, e-mail: gribov@depni.sinp.msu.ru
Изменения в версиях. Задачи клиента. Программное решение.
Полная версия раздела задается в виде: версия.подверсия.выпуск.
Номер версии увеличивается при появлении глав, затрагивающих не рассмотренные ранее вопросы. Названия этих глав выносятся в оглавление раздела. Подверсия увеличивается, когда в существующие главы добавляются новые абзацы, рисунки, либо значительно перерабатывается имеющийся материал, существенно изменяя содержание главы. А при внесении лишь незначительных правок в существующие главы увеличивается номер выпуска.
Главной задачей клиента является обеспечение приложений данными, которые запрашиваются у одного или нескольких серверов. Для ее решения клиент должен уметь выполнять транзакции, включающие в себя несколько этапов (см. рисунок). Получив необходимые данные от приложения, клиент формирует и отправляет запросы серверам. Для каждого успешно выполненого запроса клиент ожидает ответ сервера, контролируя истечение максимального времени его получения (тайм-аут). Выдача результата производится для всех запрошенных приложением объектов (параметров), в том числе тех, которые не удалось отправить серверу, либо на которые не был получен ответ. Соответственно, каждый выдаваемый результат содержит статус завершения транзакции.
Помимо традиционных сервисов к асинхронному – в смысле сигналобезопасности – клиенту предъявляется дополнительное требование. Он должен предоставлять возможность повторно-входимого вызова. То есть с его помощью в принципе можно выполнить одну клиентскую транзакцию внутри другой. Конечно, такая возможность предполагает некоторые ограничения и должна использоваться с осторожностью. При асинхронном запросе данных у от одного и того же сервера следует убедиться, что он также в состоянии отрабатывать вложенные запросы. Далеко не все серверы поддерживают такой режим работы, даже если запрашиваются различные объекты. А вот асинхронные клиентские запросы к различным серверам как правило вполне допустимы. При этом, однако, может потребоваться увеличение времени тайм-аута отдельных этапов транзакции.
#define SIZE_CLTWORK 16
Параметр SIZE_CLTWORK задает размер буфера клиентских транзакций. Каждому объекту транзакции (запросу клиента) выделятся один элемент буфера. Поэтому значение SIZE_CLTWORK определяет максимальное число как одновременно проходящих однопараметрических транзакций, так и число запрашиваемых параметров в рамках одной многопараметрической транзакции.
#define ERROR -1 #define OK 1 #define STATE_WAIT_REPLY 1 // Ожидание ответа сервера #define STATE_OK 0 // Нормальное завершение транзакции #define STATE_RETRIEVE_TIMEOUT -1 // Тайм-аут ответа сервера #define STATE_READ_TIMEOUT -2 // Тайм-аут чтения полученного ответа #define STATE_TRANS_TIMEOUT -3 // Тайм-аут цикла ожидания транзакции #define STATE_NOBUFFER -4 // Нет свободного элемента в буфере транзакций #define STATE_WRITERR -5 // Ошибка отправки запроса серверу #define STATE_UNABLE -6 // Невозможно выполнить транзакцию #define TIMER_PERIOD_MCS 10000 // Период таймера (мкс) #define TIMEOUT_RETRIEVE 1000000 // Тайм-аут получения ответа сервера (мкс) #define TIMEOUT_READ 4000000 // Тайм-аут чтения полученных данных (мкс) #define SLEEP_READ 50000 // Период цикла ожидания транзакции (мкс) #define PRIORITY_3 3 // Приоритет клиентского запроса данных typedef char int8; typedef unsigned char unsigned8; typedef short int16; typedef unsigned short unsigned16; typedef int int32; typedef unsigned int unsigned32; typedef struct { unsigned32 key; // Уникальный ключ (идентификатор) объекта транзакции unsigned8 data_1; unsigned16 data_2; int32 data_3; } data; // Структура объекта транзакции
Помимо собственно данных data_1, data_2, data_3 структура содержит поле ключа key, значение которого должно быть уникально для любых запрашиваемых клиентом объектов. По значению ключа клиент определяет, на какой именно запрос получен ответ сервера. Ключом может являться некоторая совокупность параметров, формирующая уникальный идентификатор объекта.
typedef struct { int16 state; // Статус транзакции data dt; } clientappl; // Структура приложения клиента
Структура приложения клиента содержит дополнительное поле статуса транзакции. Оно используется клиентом для переключения стадий транзакции, а после ее завершения содержит код результата.
typedef struct { int16 busy; // Семафор занятости буфера unsigned16 timeout; // Счетчик тайм-аута clientappl ca; } clientwork; // Структура буфера транзакции
Структура буфера транзакции клиента дополняет структуру приложения clientappl семафором занятости элемента буфера busy и счетчиком тайм-аута транзакции timeout.
clientwork cltwork[SIZE_CLTWORK]; // Буфер транзакций клиента int16 sem_reset; // Семафор контроля тайм-аута int16 flag_reset; // Cчетчик просчетов тайм-аута int32 sleep_cnt; // Счетчик времени задержки void micro_sleep(unsigned32 microseconds) // ms_0 { sleep_cnt = (microseconds / TIMER_PERIOD_MCS) + 1; while (sleep_cnt > 0); }
Функция micro_sleep(microseconds) обеспечивает временную задержку с точностью до периода вызова нижеприведенной функции periodic(). Время задержки задается в микросекундах. Такая реализация функции задержки уместна в приложениях без использования операционной системы. Если же ОС поддерживает функции “засыпания”: sleep(...), nanosleep(...) и т.п., целесообразно пользоваться именно этими функциями.
void resetwork(clientwork *cw) // rw_0 { cw->ca.state = STATE_OK; cw->timeout = TIMEOUT_RETRIEVE / TIMER_PERIOD_MCS; // rw_3 cw->busy = -1; // rw_4 }
Функция resetwork(cw) осуществляет инициализацию (сброс) элемента буфера транзакции клиента. При этом величина тайм-аута подготавливается для новой транзакции – устанавливается время ожидания ответа сервера [rw_3]. А семафор занятости элемента буфера принудительно открывается [rw_4]. Структура объекта транзакции не очищается, поскольку за её содержимое отвечает приложение клиента. Для обеспечения состоятельности буфера транзакции его инициализация не должна прерываться вызовом тайм-аут контроля client_control(). Так, если для занятого буфера – с установленным флагом busy – тайм-аут контроль будет осуществлён после выполнения оператора [rw_3], но до [rw_4] значение счетчика тайм-аута уменьшится на единицу.
void client_control(void) // cc_0 { unsigned16 cnt; sem_reset++; // cc_4 if (sem_reset != 0) { // cc_5 flag_reset++; // cc_6 sem_reset--; // cc_7 return; } for (cnt = 0; cnt < SIZE_CLTWORK; cnt++) { // cc_10 if (cltwork[cnt].busy < 0) continue; // cc_11 if (cltwork[cnt].timeout > 0) { // cc_12 cltwork[cnt].timeout--; // cc_13 } else { // cc_14 if (cltwork[cnt].ca.state == STATE_WAIT_REPLY) { // cc_15 cltwork[cnt].ca.state = STATE_RETRIEVE_TIMEOUT; // cc_16 cltwork[cnt].timeout = TIMEOUT_READ / TIMER_PERIOD_MCS; // cc_17 } else { // cc_18 resetwork(cltwork + cnt); // cc_19 } } } sem_reset--; // cc_23 } void control_lock(void) // cl_0 { sem_reset++; } void control_unlock(void) // cu_0 { sem_reset--; // cu_2 while (flag_reset > 0) { // cu_3 if (sem_reset != -1) break; // cu_4 client_control(); // cu_5 flag_reset--; // cu_6 } }
Три функции: client_control(), control_lock() и control_unlock() обеспечивают контроль тайм-аутов клиентских транзакций с возможностью досчета пропущенных циклов контроля. В функции client_control() для каждой активной транзакции - с установленным флагом busy [сc_11] - контролируется счетчик тайм-аута [сc_12], [сc_13]. В случае наступления события тайм-аута [сc_14] выполняемые действия зависят от стадии транзакции. Если клиент ожидал ответа сервера [сc_15], транзакция будет завершена со статусом тайм-аута ответа [сc_16]. Если же ответ сервера был получен, но данные не считаны приложением за отведенное время, транзакция сбрасывается [сc_19], что приведет к установу статуса тайм-аута чтения полученного ответа. Обратите внимание, что при обработке условия [сc_15] устанавливается новое значение счетчика, соответствующее тайм-ауту чтения полученного ответа [сc_17]. Поэтому если приложение не сумеет вовремя считать результат транзакции, информация о тайм-ауте ответа сервера будет утеряна. Такой подход, однако, гарантирует, что по истечении всех тайм-аутов, в том числе цикла ожидания транзакции, ее буфер будет сброшен и станет доступен для использования новыми транзакциями.
Функция client_control() выполнена в сигналобезопасном варианте (защищена семафором sem_reset [сc_4], [сc_23]). Однако, ее асинхронный вызов не предусматривается – она активируется из функции таймера, которая предотвращает повторную входимость собственными силами. Назначение этого семафора – блокировать работу функции client_control() в моменты времени, когда ее асинхронный – по отношению к самой клиентской транзакции – вызов способен привести к нежелательным результатам. А для того, чтобы во время таких блокировок, которые могут оказаться довольно частыми и длительными, избежать просчета циклов контроля, служит счетчик flag_reset. Он ведет подсчет [сc_6] пропущенных вследствие блокирования [сc_5] циклов контроля тайм-аута.
Для запрета тайм-аут контроля служит функция control_lock(). Ее содержание минимально – только закрытие семафора – и она определена исключительно для программной симметрии вызовов блокирования и деблокирования. Досчет пропущенных циклов производится именно при разрешении тайм-аут контроля функцией control_unlock(). Цикл досчета [сu_3] будет работать только при открытом состоянии семафора контроля, то есть когда функция контроля тайм-аута вновь становится активной. При обнаружении блокированного состояния семафора [сu_4] цикл [сu_3] обрывается, поскольку в этом случае мы получили бы зацикливание процедуры досчета. Причем контроль sem_reset осуществляется именно внутри цикла [сu_3] на случай, если будет осуществлен асинхронный вызов функции блокирования тайм-аут контроля control_lock().
Отметим особенности работы функции control_unlock(), связанные с ее возможным повторном вызовом при асинхронном управлении блокировками тайм-аут контроля. Если такой вызов произойдет внутри цикла [сu_3] до оператора [сu_5], по возвращении из него будет произведено дополнительное лишнее обращение к client_control(), а счетчик flag_reset получит отрицательное значение (именно для таких случаев он определен со знаком). Таким образом, для одних параметров будет произведен дополнительный цикл контроля тайм-аута, в то время как другие могут в дальнейшем не досчитаться одного цикла контроля. При повторном вызове control_unlock() после [сu_5], но до [сu_6] мы получим лишь недосчет (в будущем) одного цикла контроля, поскольку значение flag_reset станет отрицательным. Однако, поскольку параметры тайм-аута всегда выбираются с запасом, одиночный недосчет либо пересчет цикла контроля не следует считать значимым.
int16 write_data(data *dt, unsigned16 priority) { return OK ; return ERROR; }
Функция записи данных write_data(dt, priority) направляет запросы клиента серверу, организуя асинхронный приоритетный доступ к системе вывода данных. Вариант ее реализации приведен в главе “Процесс пошел”.
void server_reply(data *dt) // sr_0 { unsigned16 cnt; control_lock(); // sr_4 for (cnt = 0; cnt < SIZE_CLTWORK; cnt++) { // sr_5 if (cltwork[cnt].ca.state != STATE_WAIT_REPLY) continue; // sr_6 if (cltwork[cnt].ca.dt.key == dt->key) { // sr_7 cltwork[cnt].ca.dt = *dt; // sr_8 cltwork[cnt].ca.state = STATE_OK; // sr_9 cltwork[cnt].timeout = TIMEOUT_READ / TIMER_PERIOD_MCS; // sr_10 break; // sr_11 } } control_unlock(); // sr_14 }
Функция server_reply(dt) принимает ответы сервера. Ее вызов производится как правило асинхронно относительно выполнения других функций приложения клиента – по сигналу приема данных. Алгоритм обработки этого сигнала может быть реализован подобно тому, как описано в разделе “полное чтение данных” главы “Малые замечания”. Для исключения маловероятной, но возможной ситуции наложения приема данных от сервера на возникновениа тайм-аута ожидания этих данных контроль последнего блокируется [sr_4], [sr_14]. Обратная ситуация (прием данных от сервера во время контроля тайм-аута) невозможна, поскольку приоритет сигнала таймера – наивысший. При поступлении ответа производится линейный просмотр буфера транзакций (цикл [sr_5]) с пропуским тех из них, которые не ожидают ответа сервера [sr_6]. Из транзакций, находящихся в состоянии ожидания, выбирается та, уникальный идентификатор которой совпадает со значением, возвращаемым сервером [sr_7]. Для этой транзакции сохраняются полученные данные [sr_8], устанавливается статус нормального завершения [sr_9] и активируется счетчик тайм-аута чтения принятого ответа [sr_10]. Если походящая транзакция не обнаружена (сброшена, например, вследствие тайм-аута), полученные от сервера данные не используются.
void read_server_data(clientappl *ca) // sd_0 { unsigned16 cnt; control_lock(); // sd_4 for (cnt = 0; cnt < SIZE_CLTWORK; cnt++) { // sd_5 if (cltwork[cnt].busy < 0) continue; // sd_6 if (cltwork[cnt].ca.dt.key == ca->dt.key) { // sd_7 if (cltwork[cnt].ca.state != STATE_WAIT_REPLY) { // sd_8 *ca = cltwork[cnt].ca; // sd_9 resetwork(cltwork + cnt); // sd_10 } control_unlock(); // sd_12 return; // sd_13 } } control_unlock(); // sd_16 ca->state = STATE_READ_TIMEOUT; // sd_17 }
Функция read_server_data(ca) осуществляет переправку клиентскому приложению принятых от сервера данных. С этой целью она опрашивается монитором клиента в процессе ожидания ответа сервера. Для исключения возможного наложения считывания принятых данных и тайм-аута, контроль последнего блокируется [sd_4], [sd_12], [sd_16]. Так, если тайм-аут чтения полученного ответа сработает после проверки условия [sd_8], но до завершения копирования находящихся в буфере транзакции данных [sd_9], последние могут оказаться несостоятельными вследствие захвата освободившегося буфера новой клиентской транзакцией. Функция просматривает буфер транзакций (цикл [sd_5]) с пропуским свободных элементов [sd_6]. Из активных выбирается транзакция, уникальный идентификатор которой совпадает со значением, определеным приложением клиента [sd_7]. Если статус этой транзакции изменился (стал отличен от изначально установленного ожидания ответа сервера), все данные транзакции, а также ее итоговый статус возвращаются клиенту [sd_9]. После этого освобождается соответствующий буфер [sd_10]. Именно смена статуса укажет монитору клиента на завершение транзакции. А если ее статус не изменился, судьбу транзакции решат последующие вызовы функции [sd_13]. Когда же нужную транзакцию обнаружить не удалось, считается, что она была сброшена в результате тайм-аута чтения полученного ответа (client_control(), [сc_19]) и для клиента устанавливается соответствующий итоговый статус [sd_17].
void request_transaction(clientappl *ca) // rt_0 { unsigned16 cnt; control_lock(); // rt_4 for (cnt = 0; cnt < SIZE_CLTWORK; cnt++) { // rt_5 cltwork[cnt].busy++; // rt_6 if (cltwork[cnt].busy == 0) { // rt_7 cltwork[cnt].ca = *ca; // rt_8 cltwork[cnt].ca.state = STATE_WAIT_REPLY; // rt_9 if (write_data(&ca->dt, PRIORITY_3) == OK) { // rt_10 ca->state = STATE_WAIT_REPLY; // rt_11 } else { // rt_12 ca->state = STATE_WRITERR; // rt_13 resetwork(cltwork + cnt); // rt_14 } control_unlock(); // rt_16 return; } cltwork[cnt].busy--; // rt_19 } control_unlock(); // rt_21 ca->state = STATE_NOBUFFER; // rt_22 }
Функция request_transaction(ca) осуществляет запрос данных у сервера и тем самым инициирует транзакцию клиента. Для исключения возможного наложения захвата буфера транзакции и тайм-аута, контроль последнего блокируется [rt_4], [rt_16], [rt_21]. Так, если тайм-аут чтения, сопровождающийся сбросом буфера, сработает вслед за выполнением попытки его захвата [rt_6], значение семафора занятости буфера после выполнения оператора [rt_19] станет меньше минус 1 и он будет постоянно блокирован.
Функция запроса данных сигналобезопасно просматривает буфер транзакций (цикл [rt_5]), и при обнаружении свободного элемента [rt_6], [rt_7] производит его заполнение данными [rt_8] и установ статуса ожидания ответа сервера [rt_9]. Затем осуществляется попытка записи данных и если она выполняется удачно [rt_10], монитору клиента возвращается статус ожидания ответа сервера [rt_11], чем подтверждается успешная инициализация транзакции. При возникновении ошибки записи данных [rt_12] клиенту возвращается ее статус [rt_13], а буфер транзакции сбрасывается [rt_14]. Если не нашлось ни одного свободного буфера транзакции, монитору клиента также возвращается соответствующий статус [rt_22]. Отметим, что функция запроса данных по результатам своего выполнения обязательно должна установить определенный статус транзакции клиента.
void client_transaction(clientappl *ca) // ct_0 { unsigned32 tout; request_transaction(ca); // ct_4 if (ca->state != STATE_WAIT_REPLY) return; // ct_5 tout = 2 * TIMEOUT_RETRIEVE / SLEEP_READ; // ct_6 while (tout > 0) { // ct_7 micro_sleep(SLEEP_READ); // ct_8 read_server_data(ca); // ct_9 if (ca->state != STATE_WAIT_REPLY) return; // ct_10 tout--; // ct_11 } // ct_12 ca->state = STATE_TRANS_TIMEOUT; // ct_13 }
Повторно-входимая функция client_transaction(ca) является прикладным интерфейсом (API) асинхронных транзакций клиента. После ее выполнения приложение должно проверить статус транзакции. Если он показывает нормальное завершение - STATE_OK, это означает, что данные сервера были успешно приняты и могут использоваться приложением.
После успешного запроса данных у сервера [ct_4], [ct_5] функция переходит к выполнению монитора ожидания данных от сервера (от [ct_7] до [ct_12]). Этот монитор снабжен собственным контролем тайм-аута транзакции. Его значение устанавливается примерно вдвое большим штатного тайм-аута ожидания данных сервера [ct_6]. Необходимость такого решения обусловлена тем, что асинхронная транзакция может быть активирована в моменты времени, когда тайм-аут контроль блокирован. Если именно для этой транзакции произойдет сбой в получении данных от сервера, она будет постоянно находиться в состоянии ожидания ответа, поскольку статус транзакции при обращении к функции чтения read_server_data(ca) не будет изменяться [ct_9], [ct_10]. То есть при отсутствии собственного контроля тайм-аута монитор транзакции может зациклиться. Конечно, временная точность основанного на задержках [ct_8] собственного контроля заметно ниже таймерной, но и ситуации, когда возникают такие события, весьма редки. А для того, чтобы конечное приложение могло идентифицировать собственный тайм-аут транзакции клиента, используется отдельное значение статуса завершения [ct_13].
Отметим, что механизм подсчета пропущенных циклов контроля тайм-аута (см. функцию client_control()) как раз и срабатывает, когда новая транзакция клиента активируется во время блокировки тайм-аут контроля в прерываемой транзакции. Значение тайм-аута чтения полученных данных TIMEOUT_READ выбрано достаточно большим именно для того, чтобы прерванная транзакция могла бы завершиться успешно даже в случае однократного возникновения собственного тайм-аута вложенной транзакции.
void client_multi_transaction(clientappl *ca, unsigned16 npar) // cm_0 { unsigned16 cnt, flag; unsigned32 tout; if (npar == 0) return; // cm_5 if (sem_reset >= 0) { // cm_6 ca->state = STATE_UNABLE; // cm_7 return; } for (cnt = 0; cnt < npar; cnt++) request_transaction(&ca[cnt]); // cm_10 tout = 2 * TIMEOUT_RETRIEVE / SLEEP_READ; // cm_11 do { // cm_12 micro_sleep(SLEEP_READ); flag = 0; // cm_14 for (cnt = 0; cnt < npar; cnt++) { // cm_15 if (ca[cnt].state == STATE_WAIT_REPLY) { // cm_16 read_server_data(&ca[cnt]); // cm_17 if (ca[cnt].state == STATE_WAIT_REPLY) flag = 1; // cm_18 } } tout--; // cm_21 if (tout == 0) { // cm_22 for (cnt = 0; cnt < npar; cnt++) { // cm_23 if (ca[cnt].state == STATE_WAIT_REPLY) { // cm_24 ca[cnt].state = STATE_TRANS_TIMEOUT; // cm_25 } } return; // cm_28 } } while (flag); // cm_30 }
Функция client_multi_transaction(ca, npar) является аналогом client_transaction(ca) для многопараметрической транзакции клиента и приведена в качестве примера. Здесь используется не обязательное решение, когда в моменты блокировки тайм-аут контроля активация асинхронной транзакции не производится и возвращается статус невозможности ее выполнения [cm_6], [cm_7]. Вместе с тем здесь также используется собственный контроль тайм-аута [cm_11], [cm_21], [cm_22]. После запроса у сервера всех данных [cm_10] функция переходит к выполнению монитора транзакции - цикл [cm_12]. Основанием для выхода из цикла и завершения транзакции является наступление одного из двух событий: изменение статуса всех параметров транзакции либо собственный тайм-аут. Статус транзакции контролируется в цикле [cm_15] для каждого параметра, который все еще находится в состоянии ожидания ответа сервера [cm_16]. Если после обращения к функции чтения [cm_17] статус хотя бы одного такого параметра остается неизменным, устанавливается флаг продолжения мониторинга [cm_18]. При наступлении собственного тайм-аута транзакции [cm_21], [cm_22] для всех параметров, не вышедших из состояния ожидания ответа сервера, устанавливается статус тайм-аута цикла ожидания, после чего выполнение транзакции завершается [cm_28]. При использовании многопараметрических транзакций может потребоваться увеличение времени тайм-аутов, особенно когда возможен запрос многих параметров у одного сервера. Собственный тайм-аут [cm_11] также целесообразно увеличивать в зависимости от числа параметров транзакции.
void periodic(void) // pr_0 { if (sleep_cnt > 0) sleep_cnt--; // pr_2 client_control(); // pr_3 }
Функция periodic() активируется периодическим таймером и занимается решением двух задач. В [pr_2] обслуживается счетчик времени задержки, а в [pr_3] вызывается функция контроля тайм-аута. Сигнал таймера должен обладать высоким приоритетом и блокировать остальные сигналы на время своего выполнения.
void init_client(void) // ic_0 { unsigned16 cnt; sleep_cnt = 0; sem_reset = 0; // ic_5 flag_reset = 0; // ic_6 for (cnt = 0; cnt < SIZE_CLTWORK; cnt++) resetwork(cltwork + cnt); // ic_7 sem_reset = -1; // ic_8 }
Функция (пере)инициализации init_client() сбрасывает счетчик просчетов тайм-аута [ic_6] и буферы транзакций [iс_7]. Затем открывается семафор тайм-аута [ic_8]. На время переинициализации контроль тайм-аута блокируется семафором sem_reset [ic_5]. Вызов init_client() должен выполняться только извне по отношению к функциям транзакции клиента, например, из монитора прикладной программы. Транзакции, активированные во время прохождения переинициализации, либо не смогут начать выполнение (решение [cm_6]), либо будут проходить без штатного контроля тайм-аута.