還有人就算插 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 模組,範例如下:
1 2 3 4 5 6 7 | 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/ 實驗看看:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | -- get_demo.lua require 'nixio' env = nixio.getenv() html1 = [[ <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 挖一段程式過來就解決了:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 | -- 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 = [[ <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 相同的部份為了節省版面予以略過):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | -- 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 = [[ <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> <input id= "username" type= "text" name= "username" /> <label><b>Password</b></label> <input id= "password" type= "password" name= "password" /> <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 補一下