還有人就算插 SD card 也要把 PHP 移植到 7688,畢竟 PHP 比 React 要容易上手多了。正確的說,React 是前端 JavaScript library,但要看懂 React 如何與後端銜接的學習曲線實在太長了,對於筆者來說,又不需要搞到那麼 fancy。
最誇張的作法是把 7688 原本的 web server(uhttpd) listening port 從 80 改成別的(例如 5566),然後放自己寫的 web server,或者是拿 Boa 等嵌入式 Web Server 替代。
不過我們知道 7688 有內建時下最流行的 Python,那用他來做 CGI 可行嗎?
筆者實驗的結果很不理想,因為 Python 載入需要太多時間,就算"hello, world" 也一樣:
但是很多人都忘了7688 還支援 Lua,同樣的 "hello, world" 在 Lua 上快如閃電:
其實你去觀察 7688 上的 OpenWrt Web console,他就是用 Lua 寫成的:
在 OpenWrt 官方文件也提到:
「...uHTTPd supports running Lua in-process, which can speed up Lua CGI scripts...」
所以說用 Lua 實做 7688 Web CGI 才是最佳選擇,很少看到這方面文章的原因可能是因為這語言有點冷門,或者是參考資料太少。今天就來公開實做 7688 Lua Web CGI 的幾個關鍵步驟。
環境設置
編輯 /etc/config/uhttpd,加入下圖紅色方框的部份(假如您堅持要用 Python 也請參照下圖)
HTTP Protocol in ten minutes
再繼續往下之前,必須先對 HTTP protocol 有些粗淺的理解與回顧。Web Browser 與 Web Server 之間基本通訊流程如下圖:
CGI 要如何取得 GET or POST 夾帶的資訊?讓筆者引用之前文章的圖:
接著就來示範 Lua CGI script 如何取得環境變數(environment variables),與標準輸入(stdin),與輸出 html。
通常 Browser 會對 Server 發送兩種 Reqest:
- GET
- POST
上圖範例是 GET,POST 如下圖所示:
GET
由上圖得知,GET 把 query string、cookie 等資訊藏在環境變數裡。7688 Lua 取得環境變數需要引用 nixio 模組,範例如下:
require 'nixio' env = nixio.getenv() print("env type = " .. type(env)) for k, v in pairs(env) do print("key=" .. k .. " value=" .. v) end輸出結果如下:
上圖 nixio.getenv() 取出的環境變數沒有與 set 指令列出的環境變數一一對應,原因是 set 指令除了取出全域環境變數外也會取出 shell 自帶變數。
接著我們馬上打造一個簡單的 Lua CGI script 放到目錄 /www/ 實驗看看:
-- get_demo.lua require 'nixio' env = nixio.getenv() html1 = [[ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>GET Demo</title> </head> <body> ]] html2 = [[ </body> </html> ]] io.write("Content-Type: text/html\r\n\r\n") io.write(html1) for k, v in pairs(env) do print("key=" .. k .. " value=" .. v .. '<br⁄>') end io.write(html2)得到很多有用的資訊:
如果您用過 PHP,就會知道 PHP 會預先幫你把 query string 轉成 associative array,然後用 $_GET 取出即可。Lua 沒那麼方便,不過這只要直接從 OpenWrt luci 挖一段程式過來就解決了:
-- get_demo2.lua require 'nixio' function urldecode( str, no_plus ) local function __chrdec( hex ) return string.char( tonumber( hex, 16 ) ) end if type(str) == "string" then if not no_plus then str = str:gsub( "+", " " ) end str = str:gsub( "%%([a-fA-F0-9][a-fA-F0-9])", __chrdec ) end return str end function urldecode_params(url, tbl) local params = tbl or { } if url:find("?") then url = url:gsub( "^.+%?([^?]+)", "%1" ) end for pair in url:gmatch( "[^&;]+" ) do -- find key and value local key = urldecode( pair:match("^([^=]+)") ) local val = urldecode( pair:match("^[^=]+=(.+)$") ) -- store if type(key) == "string" and key:len() > 0 then if type(val) ~= "string" then val = "" end if not params[key] then params[key] = val elseif type(params[key]) ~= "table" then params[key] = { params[key], val } else table.insert( params[key], val ) end end end return params end html1 = [[ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>GET Demo</title> </head> <body> ]] html2 = [[ </body> </html> ]] io.write("Content-Type: text/html\r\n\r\n") io.write(html1) env = nixio.getenv() _get = urldecode_params(env.QUERY_STRING) for k, v in pairs(_get) do print("key=" .. k .. " value=" .. v .. '<br⁄>') end io.write(html2)輸出結果:
POST
需要 POST 的場合最常見的就是 web form。比方說 login,在 <form>...</form> 內輸入的 id, passoword 通常會以 application/x-www-form-urlencoded 編碼後放到 HTTP Reqeust - message body 內:
uHTTPD 會把這段訊息放到標準輸入(stdin)讓 CGI script 可以讀取,訊息長度則放在環境變數 CONTENT_LENGTH。下面是一個簡單的 login form 示範(與 GET 相同的部份為了節省版面予以略過):
-- post_deom.lua require 'nixio' statusmsg = { [200] = "OK", [206] = "Partial Content", [301] = "Moved Permanently", [302] = "Found", [304] = "Not Modified", [400] = "Bad Request", [401] = "Authorization Required", [403] = "Forbidden", [404] = "Not Found", [405] = "Method Not Allowed", [408] = "Request Time-out", [411] = "Length Required", [412] = "Precondition Failed", [416] = "Requested range not satisfiable", [500] = "Internal Server Error", [503] = "Server Unavailable", } function urldecode( str, no_plus ) -- .... end function urldecode_params(url, tbl) -- .... end function get_post_data(env) if env == nil or env.CONTENT_LENGTH == nil or type(env.CONTENT_LENGTH) ~= "string" then return {} end local len = tonumber(env.CONTENT_LENGTH) if len == nil or len <= 0 then return {} end return urldecode_params(io.read(len)) end login_form = [[ <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>login test</title> </head> <body> <form action="login.lua" method="post"> <label><b>Username</b></label><br/> <input id="username" type="text" name="username"/><br/> <label><b>Password</b></label><br/> <input id="password" type="password" name="password"/><br/> <input type="submit" value="Login"> </form> </body> </html> ]] local env = nixio.getenv() local post_data = get_post_data(env) if post_data.username and post_data.password then if post_data.username == 'root' and post_data.password == '12345678' then io.write("Status: " .. "302" .. " " .. statusmsg[302] .. "\r\n") io.write("Location: rpc_demo.html\r\n") else io.write("Content-Type: text/html\r\n\r\n") io.write(login_form) end else io.write("Content-Type: text/html\r\n\r\n") io.write(login_form) end運行結果:
上圖 rpc_demo.html 是聯發科提供的範例。
眼尖的讀者應該會發現這個 post_demo.lua 還沒有能力連結到真正的 id, password,請大家多多直持本 blog,讓筆者有動力繼續發表下一篇,感恩~
讚!加油!
回覆刪除感恩,請多多支持本blog
刪除這方面的文章不多 謝謝分享
回覆刪除不客氣,請繼續支持小弟,感恩喔
刪除讚哦~
回覆刪除我也還在學。
我們要用LUA做出一個匣道器的控制介面。 看到您的介紹, 受益斐淺!
Many thanks.
不客氣,請繼續支持小弟,感恩喔
刪除非常感謝,正在用7688開發一個網頁設定畫面,獲益良多!
回覆刪除歡迎呷好道相報
刪除編譯前要改動哪個地方讓它直接進入Luci而不要去LinkIt Smart的登入畫面
回覆刪除謝謝
編譯?不需要吧,直接把LinkIt Smart預設登入畫面替換掉即可
刪除有點不大知道怎麼做
刪除是換掉路徑www下的index.html?
因為手邊沒有7688,僅就記憶來回答,您想想,既然首頁是用React做的,然後也可以連到 Luci,那就僅是路徑的差別而已。有一個方法也許可行(沒實驗過),就是不要搬移 Luci,用個 symbolic link 連結過去。
刪除很受用,我在op15的系统下面,照着写了,lua demo.lua,有效,但是在游览器访问的时候,出错了。放在/www目录下,直接文本显示;放在/www/cgi-bin/目录下面,报错,说cgi的格式错误,不过很受益,谢谢楼主
回覆刪除感謝支持,抱歉沒用過OP15,不過給您兩點方向參考看看:
刪除1. 修改已經存在的 .lua 看能不能跑出點東西
2. 檢查您自己的 .lua 看有沒有執行權限,沒有就用 chmod 補一下