© 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]), либо будут проходить без штатного контроля тайм-аута.