Skip to content
LIVE FEED

BlogKamailio Giai đoạn K2 chi tiết: KEMI chuyên sâu với Lua, namespace KSR và file kamailio.lua đầy đủ

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.cfg thuầ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 cfgTên hàm Lua được gọiCá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ự đặtTuỳ 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ầnKEMI (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ử

  1. Kiểm tra cú pháp Lua độc lập trước khi nạp vào Kamailio:
    luac -p kamailio.lua
    
  2. Kiểm tra cú pháp cfg như bình thường: kamailio -c -f kamailio.cfg
  3. 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
    
  4. 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 KSR giả (mock) trong một file Lua test riêng (dùng busted hoặc luaunit), gọi thẳng ksr_request_route() với các giá trị $rm giả 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ứngNguyê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 LuaQuê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ọiDù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àiGọ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_route viế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).

Contact
Facebook
TOP