3.2 Розпарсене повідомлення¶
[!IMPORTANT] Kamailio не парсить SIP-повідомлення повністю на прийомі. Він робить мінімум, щоб зрозуміти, що повідомлення well-formed і якого воно типу. Заголовки, URI, тіло парсяться на першу вимогу. Це найбільша причина, чому route'и, що чіпають кілька заголовків, працюють так швидко.
struct sip_msg¶
Кожне отримане SIP-повідомлення живе в struct sip_msg, виділеній у pkg-купі воркера (див. розділ 2.2). Це центральна структура даних обробки повідомлень — кожна функція модуля приймає її першим аргументом, кожна псевдо-змінна — це по суті getter на її поля, кожен lump висить на ній.
Що в ній (схематично):
struct sip_msg {
char *buf; // вказівник на оригінальні отримані байти
unsigned len; // довжина buf
msg_type type; // запит чи відповідь
method_id first_line; // метод+URI (запит) або статус+reason (відповідь)
hdr_field *headers; // linked list розпарсених заголовків
hdr_field *callid; // shortcut → заголовок Call-ID (NULL до парсингу)
hdr_field *to; // shortcut → заголовок To
hdr_field *from; // shortcut → заголовок From
hdr_field *cseq;
hdr_field *contact;
/* …ще десяток shortcut'ів… */
str body; // вказівник + довжина в buf; парсинг тіла окремий
lump *add_rm; // lumps для форвард-повідомлення (наступний розділ)
lump_rpl *reply_lump; // lumps для відповіді
/* …flag'и, dst_addr, send_socket, force_send_socket, … */
};
Сама структура маленька. Байти, на які вона вказує — buf — це оригінал отриманого повідомлення, нерухомий. Ніщо в рантаймі не копіює буфер; усе — це offset'и та вказівники в одні й ті самі байти.
[!TIP] Коли ви пишете
$ru(request URI),$tu(To URI),$hdr(X-Foo)у routing-скрипті — ви тригерите Kamailio розпарсити на першу вимогу, якщо ще не. Getter псевдо-змінної дзвонитьparse_uri(),parse_to()чиparse_headers(msg, HDR_X_F)під капотом, результат кешується наsip_msg, і всі наступні звернення вже безкоштовні.
First-pass-парсинг¶
Коли receive_msg() працює (див. розділ 3.1), він дзвонить parse_msg(), який робить лише це:
- Знаходить першу строку — рядок запиту (
METHOD URI SIP/2.0) або status-рядок (SIP/2.0 STATUS REASON). Перевіряє, що вона парситься. - Йде по header-секції, реєструє в linked list ім'я і byte range кожного заголовка. Значення заголовків не парсяться. Парсер знає, де починається й закінчується
To:, але не знає, хто там. - Визначає offset і довжину тіла, якщо є
Content-Length.
Усе. Після того, як parse_msg() повернувся, ви знаєте, що це INVITE на sip:bob@example.com, — але якщо запитаєте «який URI у To?», отримаєте NULL, поки щось явно його не розпарсить.
Це не лінь — це несуча конструкція. Типовий request_route — це 30 рядків скрипта, що чіпають $ru, $tu, можливо $hdr(Authorization), і форвардять. Парсити повністю кожен заголовок на кожному повідомленні — Via, Record-Route, Contact, Allow, Supported, User-Agent, ще десятки — це коштувало б на порядок більше CPU per message, без жодної користі.
Парсинг на вимогу¶
Коли скрипту (чи функції модуля) потрібен конкретний заголовок, він дзвонить одну з entry point'ів парсера:
parse_headers(msg, HDR_TO_F, 0); // розпарсити аж до To включно
parse_to(msg); // розпарсити значення To у addr_body
parse_uri(uri.s, uri.len, &parsed); // розпарсити URI-строку в struct sip_uri
parse_body(msg); // розпарсити SDP чи інше тіло
parse_headers() приймає бітмаску того, що вам треба. Ходить по header-списку, парсить значення тих, що матчать біт у масці, поки не розпарсить усе запитане. Робота кумулятивна: розпарсити HDR_TO_F | HDR_FROM_F після того, як уже розпарсене HDR_TO_F, — це лише робота для From.
Результат кожного парсингу кешується на sip_msg. Shortcut-вказівник (msg->to, msg->from) виставляється, поле parsed структури hdr_field тримає типізоване значення (to_body, from_body і т. д.), і подальші звернення повертають миттєво.
Як виглядає «розпарсене»¶
hdr_field після парсингу тримає дві речі:
- Оригінальні байти —
name.s/name.lenдля імені,body.s/body.lenдля значення, обидва якstr-вказівники в буфер повідомлення. - Типізований вказівник
parsed, downcast залежно від типу —to_body*дляTo,via_body*дляViaі т. д.
Типізована форма тримає попередньо витягнуті поля. Для To: display name, URI, tag, parameters. Для Via: protocol, host, port, branch parameter, received parameter, далі. Ці поля теж — вказівники в оригінальний буфер, не копії: tag у To — це просто пара (ptr, len) у байти, які прилетіли з мережі.
Тіло — той самий патерн. parse_body() розпізнає content-type ('application/sdp' найчастіше), парсить тіло в типізоване дерево (для SDP: sdp_info із сесіями, стрімами, media descriptions), кешує. Модулі, що чіпають тіло (rtpengine, sdpops, presence), тригерять цей парсинг.
Чому нічого не копіюється¶
Архітектурний вибір, що зв'язує все це: оригінальний буфер ніколи не копіюється і не модифікується під час обробки. Кожна типізована структура вказує назад на byte range'и в msg->buf. body.s заголовка — це вказівник у buf. uri.s у To — це вказівник у buf. Тіло — теж вказівник у buf.
Саме це й робить per-message-обробку дешевою:
- Парсинг конкретного заголовка коштує O(довжина заголовка) раз, далі — O(1) на кожен доступ.
- Загальний memory footprint — це
len(message) + sizeof(чіпнутих parsed-структур), не множник від розміру повідомлення. - Звільнення повідомлення наприкінці route'у — одна операція: free
buf, freesip_msgі все pkg-allocated під ним.
Але це й накладає обмеження на форму мутацій. Ви не можете редагувати msg->buf на місці — кожен закешований вказівник або стане stale, або муситиме перерезолвитися. Ви не можете замінити buf на модифіковану копію без інвалідизації всіх кешованих парсінгів і всіх модулів, що вже взяли вказівник.
Те, як Kamailio розв'язує це обмеження — як він дозволяє додавати, видаляти й переписувати частини повідомлення, ніколи не торкаючись буфера — і є lump-система, на яку повністю присвячений наступний розділ.