上次推荐了一次服务器框架,然而我用的时候内存不足一直报错,唉,自己写一个吧。 这次我们将完成一个服务器框架,让我们能更方便的使用网页来控制nodemcu。
首先我们先想一下,我们想让服务器做些什么?
简单的服务器 让我们从上次的服务器开始说好了
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 function parseRequest (payload) payload = urlDecode(payload) local req = {} local _GET = {} local _, _, method, path , query = string .find (payload, "([A-Z]+) (.+)?(.+) HTTP" ) if (method == nil ) then _, _, method, path = string .find (payload, "([A-Z]+) (.+) HTTP" ) end if (query ~= nil ) then for k, v in string .gmatch (query, "([^&#]+)=([^&#]*)&*" ) do _GET[k] = v end end req.method = method req.query = _GET req.path = path return req end srv = net.createServer(net.TCP) srv:listen(80 , function (conn) conn:on("receive" , function (conn, payload) print (payload) local req = parseRequest(payload) local led_status = req.query.led if (led_status) then gpio.write (LED, led_status == "off" and 1 or 0 ) end body = "<h1> Hello, NodeMcu.</h1>" .. "<p>" .. "<span>LED开关</span>" .. "<a href='/?led=on'><input type='button' value='开'></a>" .. "<a href='/?led=off'><input type='button' value='关'></a>" .. "</p>" status = "HTTP/1.1 200 OK" .. "\r\n" type_ = "Content-Type: text/html" .. "\r\n" length = "Content-Length: " .. string .len (body) .. "\r\n" conn:send(status .. type_ .. length .. "\r\n" .. body) end ) end )
之前的代码我们先是接收了一个url,进行解析之后,我们使用其中的参数来操控了led灯,并且发送了一段文本,也就是说,一个基本的服务器需要完成以下工作。
接收参数
解析参数
发送信息
整理响应头
发送文本
发送文件
操作后台
所以简单来说,我们只要完成这几个功能就好了。
接收参数 解析参数的部分,我们已经有一个parseRequest
了,考虑到可能有中文,我们会收到%20%31
这样的参数,我们加一个解码url的函数
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 function urlDecode (url) return url:gsub ('%%(%x%x)' , function (x) return string .char (tonumber (x, 16 )) end ) end function parseRequest (payload) payload = urlDecode(payload) local req = {} local _GET = {} local _, _, method, path , query = string .find (payload, "([A-Z]+) (.+)?(.+) HTTP" ) if (method == nil ) then _, _, method, path = string .find (payload, "([A-Z]+) (.+) HTTP" ) end if (query ~= nil ) then for k, v in string .gmatch (query, "([^&#]+)=([^&#]*)&*" ) do _GET[k] = v end end req.method = method req.query = _GET req.path = path return req end
接收参数,完。
发送信息 首先我们要知道我们发送的消息具体有些什么内容
1 2 3 4 5 HTTP/1.1 200 OK Content-Type: text/html Content-Length: 68 <html>这里是一段html代码, 或者这是一个html文件</html>
也就是说我们要发送4个信息status
, type
, length
, content
。
发送文本 当我们发送的content是一段文本,我们按照之前的写法就行。 要说明的是,我们发送文本的时候最好分段发送,因为我们不能确定文本有多大,万一有几MB的大小,内存明显是不够用的。
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 function srvSend (conn, body, o) local status , type_, length o = o or {} status = o.status or "200 OK" type_ = o.type or "text/html" length = string .len (body) local buf = "HTTP/1.1 " .. status .. "\r\n" .. "Content-Type: " .. type_ .. "\r\n" .. "Content-Length: " .. length .. "\r\n" .. "\r\n" .. body local function dosend () if buf == "" then srvClose(conn) else conn:send(string .sub (buf, 1 , 1024 )) buf = string .sub (buf, 1025 ) end end dosend() conn:on("sent" , dosend) end function srvClose (conn) conn:on('sent' , function () end ) conn:on('receive' , function () end ) conn:close () conn = nil collectgarbage () end
发送文件 当我们发送的是一个文件,我们不能把文件和字符串连接起来,所以要先发送响应消息再发送文件。 同样我们发送文件时也应该分段发送。
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 function srvSendFile (conn, filename, o) local status , type_, length o = o or {} if not file.exists(filename) then status = "404 Not Found" srvSend(conn, status , { status =status }) return end status = o.status or "200 OK" type_ = o.type or "text/html" file.open (filename,"r" ) length = file.seek("end" ) file.close () local header = "HTTP/1.1 " .. status .. "\r\n" .. "Content-Type: " .. type_ .. "\r\n" .. "Content-Length: " .. length .. "\r\n" .. "\r\n" local pos = 0 ; local function dosend () file.open (filename, "r" ) if (file.seek("set" , pos) == nil ) then srvClose(conn) else local buf2 = file.read (1024 ) conn:send(buf2) pos = pos + 1024 end file.close () end conn:send(header) conn:on("sent" , dosend) end function parsePath (conn, path) local filename = "" if path == "/" then filename = "index.html" else filename = string .gsub (string .sub (path , 2 ), "/" , "_" ) end srvSendFile(conn, filename) end
这样我们就可以发送index.html
之类的文件了。
状态码和MIME类型 这里提供别人框架里的两个函数,可以方便的设置Status Code
和Content-Type
,按需使用吧。
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 function getStatusCode (code) local status = { [1 ] = 'Informational' , [2 ] = 'Success' , [3 ] = 'Redirection' , [4 ] = 'Client Error' , [5 ] = 'Server Error' , [200 ] = 'OK' , [301 ] = 'Moved Permanently' , [302 ] = 'Found' , [403 ] = 'Forbidden' , [404 ] = 'Not Found' } local msg = status [code] or status [math .floor (code / 100 )] or 'Unknow' return code .. ' ' .. msg end function getContentType (filename) local contentTypes = { ['.css' ] = 'text/css' , ['.js' ] = 'application/javascript' , ['.html' ] = 'text/html' , ['.png' ] = 'image/png' , ['.jpg' ] = 'image/jpeg' } for ext, type in pairs (contentTypes) do if string .sub (filename, -string .len (ext)) == ext then return type end end return 'text/plain' end
基本的服务器框架 有了上面的组件,我们最算完整一个基本的服务器框架了,剩下的地方很简单,几行代码就完成了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function urlDecode (url) function parseRequest (payload) function srvClose (conn) function srvSend (conn, body, o) function srvSendFile (conn, filename, o) function parsePath (conn, path) srv = net.createServer(net.TCP) srv:listen(80 , function (conn) conn:on("receive" , function (conn, payload) local req = parseRequest(payload) parsePath(conn, req.path ) end ) end )
我们可以往nodemcu里放几个文件,例如index.html
啦,logo.jpg
啦。
完美。
操作后台 好了,现在我们到了最后的部分,还是拿LED说事吧,跟之前一样,假设我们发送的参数为led=on
就点亮LED,但这次加一个条件,只有当我们的路径为/config
时才会生效,也就是
1 192.168.0.100/config?led=on
相应的服务器我们就可以这样写
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 srv = net.createServer(net.TCP) srv:listen(80 , function (conn) conn:on("receive" , function (conn, payload) local req = parseRequest(payload) if (req.path == "/config" ) then local led_status = req.query.led if (led_status) then gpio.write (LED, led_status == "off" and 1 or 0 ) end parsePath(conn, "/index.html" ) else parsePath(conn, req.path ) end end ) end )
相应的html的部分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 <!DOCTYPE HTML > <html lang ="zh-CN" > <head > <meta charset ="utf-8" > <title > Hello NodeMCU</title > </head > <body > <h1 > Hello NodeMCU!</h1 > <p > <img src ="./logo.jpg" alt ="示例图片" > <span > LED开关</span > <a href ='/config?led=on' > <input type ='button' value ='开' > </a > <a href ='/config?led=off' > <input type ='button' value ='关' > </a > </p > </body > </html >
当然,如果我们想要设置多个路径,实现更复杂的操作,我们可以这样写
1 2 3 4 5 6 7 8 9 if (req.path == "/config" ) then blabla... elseif (req.path == "/config2" ) then blabla... elseif (req.path == "/config3" ) then blabla... else parsePath(conn, req.path ) end
这样就完成了后台的操作。
顺带一提,网页中推荐使用ajax,好处是可以局部刷新网页
哎呀都4月8号了,拖延症太厉害了…话说明明deadline都快到了怎么效率还是那么低,说好的deadline是第一生产力呢?