BlogKamailio Giai đoạn K2 chi tiết: KEMI chuyên sâu với Lua, namespace KSR và file kamailio.lua đầy đủ
Bố cục bài viết
Trang này
Ngày tạo
16/09/2026
Cập nhật
Lượt xem
6
Đọc
9m
Kamailio Giai đoạn K2 — KEMI chuyên sâu (Chi tiết đầy đủ)
Điều kiện tiên quyết: đã xong Giai đoạn K0-K1 — hiểu bảng ánh xạ module, đã chạy được bản
kamailio.cfgthuần port từ OpenSIPS. Tài liệu này viết lại toàn bộ logic đó bằng Lua thật qua KEMI, và đi sâu vào cơ chế bên trong.
1. Core gọi vào script Lua như thế nào — bảng ánh xạ chính xác
KEMI không phải “một cách viết cfg khác” — nó là cơ chế Kamailio core gọi trực tiếp vào một hàm Lua có tên quy ước thay vì parse route block trong cfg. Bảng ánh xạ đúng theo mã nguồn Kamailio:
| Loại route trong cfg | Tên hàm Lua được gọi | Cách đăng ký |
|---|---|---|
request_route{} | ksr_request_route() | Tự động, không cần khai báo gì thêm |
reply_route{} (core) | ksr_reply_route() | Tự động |
onsend_route{} | ksr_onsend_route() | Tự động |
branch_route[NAME]{} | Tên hàm tự đặt | Đăng ký động bằng KSR.tm.t_on_branch("tên_hàm") |
failure_route[NAME]{} | Tên hàm tự đặt | Đăng ký động bằng KSR.tm.t_on_failure("tên_hàm") |
onreply_route[NAME]{} | Tên hàm tự đặt | Đăng ký động bằng KSR.tm.t_on_reply("tên_hàm") |
event_route[NAME]{} | Tên hàm tự đặt | Tuỳ module — một số module (ví dụ topos) có tham số riêng như event_callback để chỉ thẳng tên hàm KEMI thay vì dùng event_route[] |
Điểm khác biệt tư duy quan trọng: trong cfg thuần, route[BRANCH_X]{} là khai báo tĩnh, gắn cứng vào cfg. Trong KEMI, bạn phải chủ động gọi KSR.tm.t_on_branch("ksr_branch_manage") mỗi lần trước khi t_relay() để nói cho Kamailio biết “lần forward này, nếu cần chạy branch_route thì gọi hàm này” — nếu quên gọi, branch/failure/onreply route sẽ không bao giờ chạy dù bạn có định nghĩa hàm Lua đó.
2. Kích hoạt KEMI
loadmodule "app_lua.so"
modparam("app_lua", "load", "/etc/kamailio/kamailio.lua")
Khi đã cấu hình app_lua với load, bạn có thể để phần request_route{} trong kamailio.cfg rỗng hoặc lược bỏ hoàn toàn — toàn bộ logic chuyển sang sống trong file .lua. File kamailio.cfg lúc này chỉ còn nhiệm vụ khai báo global parameters và loadmodule/modparam.
3. Namespace KSR — quy tắc và bảng tra cứu
Quy tắc chung: một hàm được export bởi module tên xyz sẽ xuất hiện dưới dạng KSR.xyz.tên_hàm(...). Các hàm thuộc core (không thuộc module nào) thường xuất hiện trực tiếp trên KSR hoặc dưới KSR.core, ví dụ predicate kiểm tra method:
| Cfg thuần | KEMI (Lua) |
|---|---|
t_relay() | KSR.tm.t_relay() |
t_check_trans() | KSR.tm.t_check_trans() |
t_newtran() | KSR.tm.t_newtran() |
sl_send_reply("404","Not Found") | KSR.sl.sl_send_reply(404, "Not Found") |
sl_reply_error() | KSR.sl.sl_reply_error() |
record_route() | KSR.rr.record_route() |
loose_route() | KSR.rr.loose_route() |
save("location") | KSR.registrar.save("location") |
lookup("location") | KSR.registrar.lookup("location") |
www_authorize("", "subscriber") | KSR.auth_db.www_authorize("", "subscriber") |
www_challenge("", "0") | KSR.auth_db.www_challenge("", "0") |
mf_process_maxfwd_header(10) | KSR.maxfwd.process_maxfwd_header(10) |
is_method("CANCEL") | so sánh trực tiếp: KSR.pv.get("$rm") == "CANCEL", hoặc KSR.is_CANCEL() |
has_totag() | KSR.siputils.has_totag() (thuộc module siputils) |
setflag(1) | KSR.setflag(1) — flag là core, gọi trực tiếp trên KSR |
$var(x), $avp(x), $rU… | KSR.pv.get("$rU") / KSR.pv.seti("$var(x)", 5) / KSR.pv.sets(...) |
Lưu ý bắt buộc: đây là bảng tham khảo dựa trên các ví dụ chính thức của dự án — tên submodule chính xác tuyệt đối theo đúng phiên bản Kamailio bạn cài phải đối chiếu tài liệu KEMI của module đó (https://kamailio.org/docs/modules/<version>/modules_k...), vì không phải module nào cũng export toàn bộ hàm qua KEMI ở mọi phiên bản.
4. Truy cập pseudo-variable — khác biệt bắt buộc phải nhớ
Lua không hiểu cú pháp $rU, $avp(x) theo kiểu native — mọi thao tác pseudo-variable phải đi qua object KSR.pv:
local method = KSR.pv.get("$rm") -- đọc giá trị
KSR.pv.seti("$var(count)", 5) -- ghi số nguyên
KSR.pv.sets("$avp(name)", "alice") -- ghi chuỗi
KSR.pv.unset("$avp(name)") -- xoá
5. return vs exit — điểm dễ sai nhất khi mới chuyển sang Lua
Trong cfg thuần, exit; dừng ngay lập tức toàn bộ xử lý route, bất kể đang lồng bao nhiêu tầng if. Trong Lua, return chỉ thoát khỏi hàm hiện tại — nếu bạn gọi một hàm phụ (helper function) và muốn dừng hẳn xử lý bản tin ngay tại đó (không chỉ thoát khỏi helper rồi quay lại hàm gọi), phải dùng hàm core dành riêng cho việc này (một số ngôn ngữ KEMI export rõ ràng dưới dạng KSR.x.exit() hoặc tương đương) thay vì chỉ return — nếu không, code sau lời gọi helper trong hàm cha vẫn tiếp tục chạy, khác hẳn hành vi exit; trong cfg.
6. Đăng ký branch/failure/onreply route — ví dụ đầy đủ
function ksr_route_relay()
if KSR.is_INVITE() then
KSR.tm.t_on_branch("ksr_branch_manage")
KSR.tm.t_on_failure("ksr_failure_manage")
KSR.tm.t_on_reply("ksr_onreply_manage")
end
if KSR.tm.t_relay() < 0 then
KSR.sl.sl_reply_error()
end
end
function ksr_branch_manage()
KSR.info("chuẩn bị gửi branch tới: " .. KSR.pv.get("$ru") .. "\n")
end
function ksr_failure_manage()
if KSR.tmx.t_is_canceled() > 0 then
return
end
-- ví dụ: nếu 486/408, không làm gì thêm, chỉ để lỗi trả về nguyên trạng
end
function ksr_onreply_manage()
KSR.info("nhận reply: " .. KSR.pv.get("$rs") .. "\n")
end
7. Global state trong Lua — nối lại bài học SHM/PKG
Kamailio vẫn giữ nguyên mô hình multi-process đã học ở Giai đoạn 1 OpenSIPS — mỗi worker process có một interpreter Lua embedded riêng biệt. Hệ quả: một biến Lua khai báo kiểu global_var = 5 không hề được chia sẻ giữa các worker, giống hệt PKG memory không chia sẻ giữa các process. Muốn lưu trạng thái dùng chung giữa các process (đếm số cuộc gọi active toàn hệ thống, cache tạm…), vẫn phải gọi vào module dùng SHM thật:
KSR.htable.sht_seti("counters", "active_calls", 1) -- htable dùng SHM thật, chia sẻ được
local v = KSR.htable.sht_geti("counters", "active_calls")
8. Ví dụ thực chiến — tích hợp API ngoài đúng cách (lợi thế thật của KEMI)
Đây là lý do người ta chọn KEMI: gọi ra một API/webhook ngoài khi có cuộc gọi mới. Điểm mấu chốt: không dùng thư viện socket thuần của Lua để gọi HTTP đồng bộ trong route — làm vậy sẽ chặn đứng worker process đang xử lý SIP cho tới khi API trả lời, gây nghẽn toàn bộ hệ thống nếu API chậm. Cách đúng là gọi qua module http_async_client của chính Kamailio — module này dùng cơ chế t_suspend()/resume nội bộ để giải phóng worker trong lúc chờ HTTP response:
loadmodule "http_async_client.so"
modparam("http_async_client", "workers", 4)
function ksr_notify_new_call()
KSR.http_async_client.query("http://webhook.local/new-call", "ksr_webhook_cb")
end
function ksr_webhook_cb(reply)
KSR.info("webhook trả về: " .. tostring(reply) .. "\n")
end
Lưu ý về độ chính xác: chữ ký hàm chính xác (query(...) nhận bao nhiêu tham số, callback nhận object dạng gì) lệch giữa các phiên bản http_async_client — trước khi triển khai thật, đối chiếu README của module đúng version. Nguyên tắc kiến trúc (dùng module async thay vì socket đồng bộ trong Lua) mới là điều bắt buộc phải nhớ, không phải cú pháp cụ thể.
9. File KEMI đầy đủ — port lại bài lab Giai đoạn K1 hoàn toàn bằng Lua
kamailio.cfg (rút gọn, chỉ còn global + module):
#!KAMAILIO
debug=2
log_stderror=yes
children=4
mpath="/usr/lib/x86_64-linux-gnu/kamailio/modules/"
listen=udp:eth0:5060
loadmodule "tm.so"
loadmodule "sl.so"
loadmodule "rr.so"
modparam("rr", "append_fromtag", 1)
loadmodule "maxfwd.so"
loadmodule "usrloc.so"
modparam("usrloc", "db_mode", 0)
loadmodule "registrar.so"
loadmodule "auth.so"
loadmodule "auth_db.so"
modparam("auth_db", "calculate_ha1", 1)
modparam("auth_db", "password_column", "password")
modparam("auth_db", "db_url", "mysql://kamailio:kamailiorw@localhost/kamailio")
loadmodule "siputils.so"
loadmodule "acc.so"
modparam("acc", "log_flag", 1)
loadmodule "app_lua.so"
modparam("app_lua", "load", "/etc/kamailio/kamailio.lua")
kamailio.lua:
FLT_ACC = 1
function ksr_request_route()
if not (KSR.maxfwd.process_maxfwd_header(10) > 0) then
KSR.sl.sl_send_reply(483, "Too Many Hops")
return
end
if KSR.siputils.has_totag() > 0 then
if KSR.rr.loose_route() > 0 then
if KSR.is_BYE() then
KSR.setflag(FLT_ACC)
end
ksr_route_relay()
return
else
if KSR.is_ACK() then
if KSR.tm.t_check_trans() > 0 then
KSR.tm.t_relay()
end
return
end
KSR.sl.sl_send_reply(404, "Not Here")
return
end
end
if KSR.is_CANCEL() then
if KSR.tm.t_check_trans() > 0 then
KSR.tm.t_relay()
end
return
end
if KSR.is_OPTIONS() and KSR.pv.get("$rU") == nil then
KSR.sl.sl_send_reply(200, "OK")
return
end
if KSR.is_REGISTER() then
if KSR.auth_db.www_authorize("", "subscriber") < 0 then
KSR.auth_db.www_challenge("", "0")
return
end
if KSR.registrar.save("location") < 0 then
KSR.sl.sl_reply_error()
end
return
end
if KSR.is_INVITE() then
KSR.rr.record_route()
KSR.tm.t_newtran()
end
if KSR.registrar.lookup("location") < 0 then
KSR.sl.sl_send_reply(404, "Not Found")
return
end
if KSR.is_INVITE() then
KSR.setflag(FLT_ACC)
end
ksr_route_relay()
end
function ksr_route_relay()
if KSR.tm.t_relay() < 0 then
KSR.sl.sl_reply_error()
end
end
So sánh trực tiếp với bản cfg thuần ở Giai đoạn K0-K1 — cấu trúc logic giống hệt từng bước, chỉ khác cú pháp gọi hàm. Đây chính là minh chứng KEMI không phải “học lại từ đầu” mà là “dịch cùng một tư duy sang một ngôn ngữ khác”.
10. Kiểm thử
- Kiểm tra cú pháp Lua độc lập trước khi nạp vào Kamailio:
luac -p kamailio.lua - Kiểm tra cú pháp cfg như bình thường:
kamailio -c -f kamailio.cfg - Chạy foreground xem log lỗi Lua runtime (nếu gọi sai tên hàm KSR, lỗi hiện ngay trong log dạng “attempt to call a nil value”):
kamailio -f kamailio.cfg -D -E - Unit test không cần chạy Kamailio thật: đây là lợi ích lớn của KEMI — viết một bảng
KSRgiả (mock) trong một file Lua test riêng (dùngbustedhoặcluaunit), gọi thẳngksr_request_route()với các giá trị$rmgiả lập khác nhau, kiểm tra hàm nào được gọi đúng — kiểm thử logic routing mà không cần môi trường SIP thật.
11. Lỗi thường gặp
| Triệu chứng | Nguyên nhân hay gặp |
|---|---|
| Gọi hàm module nhưng báo lỗi “attempt to call a nil value” | Sai tên submodule trong KSR.<module>.<hàm> — kiểm tra đúng tên module đã loadmodule, và module đó phải hỗ trợ KEMI export |
branch_route/failure_route/onreply_route không bao giờ chạy dù đã viết hàm Lua | Quên gọi KSR.tm.t_on_branch()/t_on_failure()/t_on_reply() trước t_relay() — không giống cfg tĩnh, KEMI phải đăng ký động mỗi lần |
| Biến đếm toàn cục sai giá trị, lúc đúng lúc sai giữa các cuộc gọi | Dùng biến Lua toàn cục thường thay vì htable — mỗi worker process có Lua VM riêng, không chia sẻ được |
| Hệ thống bị treo/chậm hẳn khi tích hợp API ngoài | Gọi thư viện socket Lua đồng bộ trực tiếp trong route thay vì qua http_async_client — chặn đứng worker process |
exit;-tương-đương không dừng được xử lý khi gọi qua hàm phụ | Nhầm return trong helper function với việc dừng hẳn toàn bộ route — return chỉ thoát hàm hiện tại |
12. Tự kiểm tra trước khi qua Giai đoạn K3
- Giải thích chính xác vì sao
branch_routeviết bằng Lua không tự động chạy nếu thiếu một dòng gọi hàm cụ thể. - Viết lại được nhánh xử lý
MESSAGE(trả 202 Accepted) bằng cả hai cách: cfg thuần và Lua KEMI, chỉ ra đúng điểm khác nhau. - Giải thích được vì sao không nên gọi thư viện HTTP đồng bộ ngay trong
ksr_request_route(). - Mô tả được cách viết một unit test tối thiểu cho
ksr_request_route()mà không cần khởi động Kamailio thật.
Nếu trả lời trôi chảy cả 4 câu, bạn sẵn sàng cho Giai đoạn K3 (topos nâng cao, RTPengine, dispatcher/carrierroute trong bối cảnh Kamailio).