Новости

Асинхронний клієнт.

  1. версія 1.0.1 © Copyright 2006 Грибов Ігор, e-mail: [email protected] Зміст.
  2. Зміни в версіях.
  3. Завдання клієнта.
  4. Програмне рішення.

версія 1.0.1 © Copyright

2006 Грибов Ігор, e-mail: [email protected]

Зміст.

Зміни в версіях. Завдання клієнта. Програмне рішення.

Зміни в версіях.

Повна версія розділу задається у вигляді: версія.подверсія.випуск.

Номер версії збільшується при появі глав, які зачіпають не розглянуті раніше питання. Назви цих глав виносяться в зміст розділу. Підверсія збільшується, коли в існуючі глави додаються нові абзаци, малюнки, або значно переробляється наявний матеріал, істотно змінюючи зміст глави. А при внесенні лише незначних правок в існуючі глави збільшується номер випуску.

Завдання клієнта.

Головним завданням клієнта є забезпечення додатків даними, які запитуються у одного або декількох серверів. Для її вирішення клієнт повинен вміти виконувати транзакції, що включають в себе кілька етапів (див. Малюнок). Отримавши необхідні дані від програми, клієнт формує і відправляє запити серверам. Для кожного успішно виконаного запиту клієнт очікує відповідь сервера, контролюючи витікання максимального часу його отримання (тайм-аут). Видача результату проводиться для всіх запитаних додатком об'єктів (параметрів), в тому числі тих, які не вдалося відправити серверу, або на які не було отримано відповіді. Відповідно, кожен видається результат містить статус завершення транзакції.

Крім традиційних сервісів до асинхронного - в сенсі сігналобезопасності - клієнтові пред'являється додаткова вимога. Він повинен надавати можливість повторно-входимость виклику. Тобто з його допомогою в принципі можна виконати одну клієнтську транзакцію всередині іншої. Звичайно, така можливість передбачає деякі обмеження і повинна використовуватися з обережністю. При асинхронному запиті даних у від одного і того ж сервера слід переконатися, що він також в стані відпрацьовувати вкладені запити. Далеко не всі сервери підтримують такий режим роботи, навіть якщо запитуються різні об'єкти. А ось асинхронні клієнтські запити до різних серверів як правило цілком припустимі. При цьому, однак, може знадобитися збільшення часу тайм-ауту окремих етапів транзакції.


Програмне рішення.

#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; // Лічильник прорахунків тайм-ауту int32 sleep_cnt; // Лічильник часу затримки void micro_sleep (unsigned32 microseconds) // ms_0 {sleep_cnt = (microseconds / TIMER_PERIOD_MCS) + 1; while (sleep_cnt> 0); }

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

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

Или позвоните нам по телефонам: (048) 823-25-64

Организация (обязательно) *

Адрес доставки

Объем

Как с вами связаться:

Имя

Телефон (обязательно) *

Мобильный телефон

Ваш E-Mail

Дополнительная информация: