4、Luci控制器开发:控制器路由设计、处理HTTP请求与响应、数据模型与Session管理
好,咱们今天聊聊Luci控制器开发。说实话,这部分是OpenWrt/LEDE开发里最绕不开的一环。你想想看,一个CPE设备好不好用,用户第一眼看到的就是界面。而界面背后,就是控制器在撑场面。
我个人习惯把控制器比作「前台接待」。用户点了个按钮,控制器得知道该找谁处理,处理完了还得把结果包装好送回去。今天我们就把它拆开揉碎了讲清楚。
4.1 控制器路由设计
路由设计,说白了就是URL到处理函数的映射。Luci用的是自己那套MVC框架,路由规则写在控制器文件里。
每个控制器文件对应一个模块。比如你写一个网络诊断工具,文件名叫diagnose.lua,放在/usr/lib/lua/luci/controller/admin/下面。文件开头长这样:
module("luci.controller.admin.diagnose", package.seeall)
function index()
entry({"admin", "diagnose"}, template("admin_diagnose"), _("网络诊断"), 60)
end
这里entry()就是注册路由。第一个参数是路径层级,第二个参数是处理方式,第三个是菜单名,第四个是权重。
路径层级怎么理解?
比如{"admin", "diagnose"},访问时就是/cgi-bin/luci/admin/diagnose。层级可以嵌套,比如{"admin", "tools", "ping"},那就对应三级菜单。
重点来了:路由的第二个参数有三种常见写法:
template("xxx")—— 直接渲染模板,不需要控制器逻辑call("action_func")—— 调用控制器里的函数处理alias("other_path")—— 重定向到另一个路由
我在项目中遇到过一个问题:路由写好了,但页面死活打不开。后来发现是文件名和路径层级不匹配。Luci会按文件名自动加载,但路径层级是你自己定义的。两者不一致,就会404。嗯,这个坑我踩过两次才记住。
4.2 处理HTTP请求与响应
控制器收到请求后,怎么拿到参数?怎么返回数据?这是核心。
Luci里,HTTP请求信息存在luci.http模块里。常用的几个方法:
local http = require "luci.http"
-- 获取GET参数
local action = http.formvalue("action")
-- 获取POST表单数据
local username = http.formvalue("username")
-- 获取请求头
local user_agent = http.getenv("HTTP_USER_AGENT")
响应怎么写?
我建议统一用JSON格式返回,尤其是做AJAX交互时。前端好解析,后端也好构造。
function index()
local http = require "luci.http"
local json = require "luci.jsonc"
local result = {
code = 0,
message = "success",
data = {
status = "running",
uptime = 12345
}
}
http.prepare_content("application/json")
http.write(json.stringify(result))
end
小技巧:调试时可以用http.write()直接输出纯文本,方便看中间结果。但上线前记得删掉,不然JSON格式就乱了。
我曾经在做一个批量配置下发功能时,前端一直报解析错误。查了半天,发现是控制器里多输出了一个换行符。HTTP响应体必须严格符合格式,多一个空格都可能出问题。从那以后,我写响应前都会先清空输出缓冲区。
4.3 数据模型
数据模型这块,Luci用的是UCI(Unified Configuration Interface)。说白了,就是把配置存成文本文件,然后用API读写。
UCI的配置文件在/etc/config/目录下。比如网络配置是/etc/config/network,无线配置是/etc/config/wireless。
控制器里怎么操作UCI?
local uci = require "luci.model.uci"
local cursor = uci.cursor()
-- 读取配置
local ssid = cursor:get("wireless", "radio0", "ssid")
local encryption = cursor:get("wireless", "radio0", "encryption")
-- 修改配置
cursor:set("wireless", "radio0", "ssid", "MyCPE_5G")
cursor:set("wireless", "radio0", "encryption", "psk2")
-- 提交生效
cursor:commit("wireless")
注意:cursor:commit()只是把修改写入文件,不会立即生效。要让配置生效,还得调用对应的服务重启命令。比如无线配置改了,要执行wifi reload。
我建议把UCI操作封装成独立函数。比如:
function get_wifi_config(radio)
local uci = require "luci.model.uci"
local cursor = uci.cursor()
return {
ssid = cursor:get("wireless", radio, "ssid"),
encryption = cursor:get("wireless", radio, "encryption"),
key = cursor:get("wireless", radio, "key")
}
end
这样做的好处是,业务逻辑和UCI操作解耦。以后换配置管理方式,只需要改这个函数就行。
4.4 Session管理
Session管理,说白了就是怎么记住用户登录状态。Luci默认用文件存储Session,存在/tmp/luci-sessions/目录下。
Session的生命周期:
- 用户登录成功,Luci生成一个Session ID,存到Cookie里
- 每次请求,Luci根据Cookie里的Session ID,找到对应的Session数据
- 用户退出或超时,Session被删除
控制器里怎么读写Session?
local session = require "luci.session"
local sid = session.getsid()
-- 读取Session变量
local username = session:read("username")
local login_time = session:read("login_time")
-- 写入Session变量
session:write("last_action", "reboot")
session:write("action_time", os.time())
核心要点:Session里不要存敏感信息,比如密码。Session文件是明文存储的,任何人都能读。我一般只存用户名、登录时间、权限等级这些。
我曾经遇到过一个诡异的问题:用户登录后,过几分钟就自动退出。查了半天,发现是系统时间不对。Session超时判断依赖系统时间,如果时间跳变,Session就会失效。嗯,从那以后我写CPE固件时,第一件事就是确保NTP同步正常。
Session超时怎么配置?
在/etc/config/luci里:
config core 'main'
option session_time '3600' -- 单位:秒,默认1小时
option session_grace '300' -- 宽限期,5分钟
超时时间设太短,用户频繁重新登录;设太长,又不安全。我个人习惯给管理界面设30分钟,给普通用户设2小时。你想想看,运维人员配设备一般不会超过半小时,普通用户可能挂着页面看状态。
4.5 实战:一个完整的控制器示例
说了这么多,咱们来个完整的例子。假设我们要做一个系统重启功能:
module("luci.controller.admin.reboot", package.seeall)
function index()
entry({"admin", "reboot"}, call("action_reboot"), _("重启系统"), 80)
end
function action_reboot()
local http = require "luci.http"
local json = require "luci.jsonc"
local session = require "luci.session"
-- 检查Session是否有效
if not session.isvalid() then
http.prepare_content("application/json")
http.write(json.stringify({
code = -1,
message = "会话已过期,请重新登录"
}))
return
end
-- 执行重启
local result = os.execute("reboot")
http.prepare_content("application/json")
if result == 0 then
http.write(json.stringify({
code = 0,
message = "重启命令已执行"
}))
else
http.write(json.stringify({
code = 1,
message = "重启失败"
}))
end
end
这个例子涵盖了路由注册、Session校验、HTTP响应三个核心点。你写控制器时,基本就是照着这个模板来。
最后说一句:控制器开发最忌讳「一把梭」。路由、请求处理、数据模型、Session管理,每个环节都要想清楚。我见过太多人把业务逻辑全塞进模板文件里,结果后期维护想死的心都有。记住:控制器只做调度和校验,真正的业务逻辑放到独立的模块里。
好了,这一章就到这里。下一章我们聊聊模板引擎和前端交互,那才是真正让界面「活」起来的部分。