3.5 Форвардинг і відповіді¶
[!IMPORTANT] «Форвардинг» — це момент, коли все накопичене досі — розпарсене повідомлення, lump-список, per-branch-стан — колапсує в реальні байти на сокеті. Це також місце, де stateless- і stateful-форвардинг різко розходяться: один — це виклик функції, інший — створення довгоживучої state machine.
Два способи форвардити, два профілі ціни¶
Kamailio підтримує два принципово різних режими форвардингу, і вибір — ваш, per-message.
Stateless (forward() / sl). Воркер:
1. Обирає destination (next hop) за request-URI та заголовками Route.
2. Просить lump-applier побудувати вихідний буфер.
3. Пише його в сокет.
4. Повністю забуває про повідомлення. Жодного збереженого стану.
Жодних транзакцій, жодних retransmission-таймерів, жодної свідомості того, чи воно дійшло. Якщо прилетить відповідь — то це просто ще одне вхідне повідомлення для голого onreply_route. Ціна: кілька мікросекунд плюс send в ядро. shm-footprint: нуль.
Stateful (t_relay() / tm). Воркер:
1. Шукає — або створює — транзакцію в hash-таблиці tm (у shm).
2. Для кожного branch'а (часто одного, але може форкати): клонує lump-сет, бігає branch_route, якщо armed, будує вихідний буфер, шле.
3. Стартує retransmission-таймери.
4. Повертає скрипту. Транзакція живе у shm, поки не завершиться.
Коли прилетить відповідь, воркер, що її підхопить, шукає транзакцію у shm, може запустити onreply_route[N] (якщо armed), і вирішує: relay'нути відповідь назад, чекати на ще branches, бігти failure_route, чи фіналізувати.
Ціна: кілька KB shm на виклик, плюс слот у timer wheel, плюс contention на per-bucket-локу при вставці/lookup'і. Вартує для будь-якого випадку, коли важливі retransmission, форки чи post-decision-логіка — себто практично завжди, крім чистого stateless-проксі.
Lump applier — момент, коли lumps стають байтами¶
Обидва режими врешті дзвонять build_req_buf_from_sip_req() (або його reply-аналог). Це функція, що лінійно йде по оригінальному msg->buf, звіряється з lump-списком і продукує свіжий буфер вихідного повідомлення. Це момент, коли кожна вставка заголовка, видалення, переписування URI чи SDP-edit, поставлені в чергу під час routing'у, нарешті стаються.
Логіка walker'а — рівно та, що була ескізована в розділі 3.3:
out_buf = новий буфер на (len(msg->buf) + сума len value у lumps
- сума len del у lumps)
іти по msg->buf лінійно, позиція i = 0..len:
якщо DEL-lump прив'язаний у i:
i += lump.len # пропустити
якщо ADD-lump прив'язаний у i:
дописати lump.value у out_buf
(резолвити маркери: IP вихідного сокета для Record-Route тощо)
дописати msg->buf[i] у out_buf
i += 1
Резолвинг маркерів — важливий. Lump, що каже «вставити Record-Route: <sip:HOST:PORT;lr>», де HOST:PORT — маркер — ці байти не заповнюються, поки не обраний вихідний сокет. Вибір залежить від destination'у, від routing-таблиці ядра, від force_send_socket() у скрипті. Резолвити в цей останній момент — це спосіб не змушувати скрипт знати, який інтерфейс він використовує.
Форк — кілька destination'ів з одного запиту¶
tm підтримує паралельні та послідовні форки. Класичний приклад: INVITE користувачу, що зареєстрований на трьох пристроях. Хочеться дзвонити всім трьом одночасно, прийняти перший 200 OK, скасувати інших.
sequenceDiagram
participant UAC
participant K as Kamailio (tm)
participant D1 as Пристрій 1
participant D2 as Пристрій 2
participant D3 as Пристрій 3
UAC->>K: INVITE
K->>K: lookup, список branch'ей
par паралельний форк
K->>D1: INVITE (branch 0)
K->>D2: INVITE (branch 1)
K->>D3: INVITE (branch 2)
end
D2-->>K: 200 OK
K-->>UAC: 200 OK
par cancel'нути інших
K->>D1: CANCEL
K->>D3: CANCEL
end
Що насправді відбувається в tm:
- Структура транзакції тримає branch-масив (зазвичай до
MAX_BRANCHES, часто 16). - На кожен branch: свій destination, свої lump-доповнення, свій retransmission-таймер, свій per-branch-стан.
- Cfg обирає destination'и — або імпліцитно (через
Contact-заголовки зusrloc), або явно (append_branch()додає branch'і передt_relay()). - Усі branch'і йдуть приблизно одночасно. Далі вони працюють незалежно, поки один не видасть final response.
branch_route[N] бігає один раз на branch, перед тим як вихідне повідомлення цього branch'а буде побудоване. Тут можна кастомізувати per branch — різне From display name на пристрій, branch-specific accounting, branch-specific timeout.
Коли прилетіла перша 2xx, tm relay'ить її UAC і починає cancel'ити інших живих branch'ей. Коли всі final responses прийшли, tm обирає «найкращу» failure-відповідь (зазвичай найнижчий 4xx, що не redirect) і relay'ить її — якщо failure_route не втрутиться раніше.
Failure-route'и і re-forking¶
failure_route[N] бігає, коли:
- Усі branch'і видали final responses, жоден не був 2xx, і транзакція ось-ось relay'не failure назад UAC.
- Або одна не-форкна транзакція отримала 4xx-6xx.
Усередині failure-route'у скрипт може:
- Побудувати кастомну відповідь через t_reply("503", "Service unavailable") — переб'є те, що було б природньо пропаговано.
- Re-fork'нути на інший destination: очистити branch-список, append_branch() з новими destination'ами, викликати t_relay() знову на тій самій транзакції.
- Нічого не робити, і тоді tm піде дефолтним шляхом і relay'не failure.
Кейс re-fork — це й є те, як реалізується operational-failover: primary trunk повернув 503 → failure_route ловить → append secondary trunk → re-relay. З точки зору UAC — це один call setup, що зайняв трохи довше за звичайний.
Reply-шлях — як знайти потрібну транзакцію¶
Коли SIP-відповідь прилітає в receive_msg(), воркер має знайти транзакцію, якій вона належить. Lookup іде за:
- Параметру branch у верхньому
Via(якийtmпоставив, коли відправляв оригінал). Call-ID,CSeq,From-tag — fallback-hash, якщо branch-параметр відсутній чи непридатний.
Цей lookup б'є в ту саму per-bucket hash-таблицю, що зберігала транзакцію. Воркер, що підхопив відповідь, може не бути тим же воркером, що відправляв оригінал — саме тому транзакція у shm. Після lookup'у:
- Якщо це provisional response (1xx) — оновити стан, бігти
onreply_routeякщо armed, переслати відповідь UAC. - Якщо це final response (2xx-6xx) — оновити стан, вирішити: чекати ще branches чи фіналізувати.
- Якщо фіналізуємо — обрати найкращу відповідь, бігти
failure_routeякщо armed і відповідь — failure, relay'нути назад.
Stateless-відповіді — коли хочеться дешево¶
Для того, чому не потрібен transaction-state — 200 OK на OPTIONS, 401 Unauthorized на неавтентифікований REGISTER — Kamailio має stateless-відповіді через модуль sl:
Це будує reply з request'а, застосовує reply-lumps, шле і повертає. Жодної транзакції не створюється. Це той самий дешевий шлях, і його варто використовувати, коли вам не потрібен transaction-level-retransmission-handling.
Що тепер має сидіти у голові¶
Повний шлях запиту від дроту до дроту:
- Прийом — ядро демультиплексує воркеру, воркер дзвонить
receive_msg(). - First-pass-парсинг —
parse_msg()знаходить першу строку і offset'и заголовків, нічого більше. - Виконання route'у —
request_routeбігає наsip_msg'і. Псевдо-змінні і функції модулів тригерять lazy-парсинг конкретних полів. Модифікації йдуть у чергу як lumps. Скрипт вирішує, куди це повідомлення. - Форвардинг — stateless
forward()чи statefult_relay(). Якщо stateful —tmстворює транзакцію у shm і форкає в branch'і. - Per-branch-обробка —
branch_route[N]корегує per branch. Lumps застосовуються. Буфер будується. Send. - Reply — врешті прилітає відповідь. Бігає
onreply_route. Стан оновлюється. Відповідь фор'юардиться назад, абоfailure_routeпереграє рішення. - Cleanup — коли транзакція завершилася (final response relay'нуто, чи всі branches відпрацювали),
tmзвільняє shm-стан. pkg-стан для повідомлення, що прилетіло, був звільнений у кінці кроку 3.
Все. Кожна інша архітектурна частина в цьому посібнику — script engine, KEMI, control plane, фішки — це уточнення на цьому циклі.
← Зміст · ← 3.4 Движок маршрутизації · Перехід до 5.1 KEMI →