上次推荐了一次服务器框架,然而我用的时候内存不足一直报错,唉,自己写一个吧。
首先我们先想一下,我们想让服务器做些什么?
简单的服务器 让我们从上次的服务器开始说好了
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是一段文本,我们按照之前的写法就行。
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是第一生产力呢?