Browse Source

Big refactoring: server now uses a separate thread that yields and then resumes on 'sent'. This allows us to serve large files. Moved serving of error pages and serving of static files into separate scripts httpserver-error.lua and httpserver-static.lua

Marcos Kirsch 9 years ago
parent
commit
77920f4a94
3 changed files with 86 additions and 47 deletions
  1. 21 0
      httpserver-error.lua
  2. 28 0
      httpserver-static.lua
  3. 37 47
      httpserver.lua

+ 21 - 0
httpserver-error.lua

@@ -0,0 +1,21 @@
+-- httpserver-error.lua
+-- Part of nodemcu-httpserver, handles sending error pages to client.
+-- Author: Marcos Kirsch
+
+local function getHTTPStatusString(code)
+   if code == 404 then return "Not Found" end
+   if code == 400 then return "Bad Request" end
+   if code == 501 then return "Not Implemented" end
+   return "Unknown HTTP status"
+end
+
+local function sendHeader(connection, code, codeString, mimeType)
+   connection:send("HTTP/1.0 " .. code .. " " .. codeString .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\nConnection: close\r\n\r\n")
+end
+
+return function (connection, args)
+   errorString = getHTTPStatusString(args.code)
+   print("Error: " .. args.code .. ": " .. errorString)
+   sendHeader(connection, args.code, errorString, "text/html")
+   connection:send("<html><head><title>" .. args.code .. " - " .. errorString .. "</title></head><body><h1>" .. args.code .. " - " .. errorString .. "</h1></body></html>\r\n")
+end

+ 28 - 0
httpserver-static.lua

@@ -0,0 +1,28 @@
+-- httpserver-static.lua
+-- Part of nodemcu-httpserver, handles sending static files to client.
+-- Author: Marcos Kirsch
+
+local function getMimeType(ext)
+   -- A few MIME types. Keep list short. If you need something that is missing, let's add it.
+   local mt = {css = "text/css",gif = "image/gif",html = "text/html",ico = "image/x-icon",jpeg = "image/jpeg",jpg = "image/jpeg",js = "application/javascript",png = "image/png"}
+   if mt[ext] then return mt[ext] else return "text/plain" end
+end
+
+local function sendHeader(connection, code, codeString, mimeType)
+   connection:send("HTTP/1.0 " .. code .. " " .. codeString .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\nConnection: close\r\n\r\n")
+end
+
+return function (connection, args)
+   print("Serving:", args.file)
+   sendHeader(connection, 200, "OK", getMimeType(args.ext))
+   file.open(args.file)
+   -- Send file in little chunks
+   while true do
+      local chunk = file.read(512)
+      if chunk == nil then break end
+      coroutine.yield()
+      connection:send(chunk)
+   end
+   print("Finished sending file.")
+   file.close()
+end

+ 37 - 47
httpserver.lua

@@ -25,12 +25,6 @@ local function uriToFilename(uri)
    return "http/" .. string.sub(uri, 2, -1)
 end
 
-local function onError(connection, errorCode, errorString)
-   print(errorCode .. ": " .. errorString)
-   connection:send("HTTP/1.0 " .. errorCode .. " " .. errorString .. "\r\nContent-Type: text/html\r\nConnection: close\r\n\r\n")
-   connection:send("<html><head><title>" .. errorCode .. " - " .. errorString .. "</title></head><body><h1>" .. errorCode .. " - " .. errorString .. "</h1></body></html>\r\n")
-end
-
 local function parseArgs(args)
    local r = {}; i=1
    if args == nil or args == "" then return r end
@@ -60,45 +54,29 @@ local function parseUri(uri)
    return r
 end
 
-local function getMimeType(ext)
-   -- A few MIME types. No need to go crazy in this list. If you need something that is missing, let's add it.
-   local mt = {}
-   mt.css = "text/css"
-   mt.gif = "image/gif"
-   mt.html = "text/html"
-   mt.ico = "image/x-icon"
-   mt.jpeg = "image/jpeg"
-   mt.jpg = "image/jpeg"
-   mt.js = "application/javascript"
-   mt.png = "image/png"
-   if mt[ext] then return mt[ext] end
-   -- default to text.
-   return "text/plain"
-end
+-- This variable holds the thread used for sending data back to the user.
+-- We do it in a separate thread because we need to yield when sending lots
+-- of data in order to avoid overflowing the mcu's buffer.
+local connectionThread
 
 local function onGet(connection, uri)
    local uri = parseUri(uri)
    local fileExists = file.open(uri.file, "r")
+   file.close()
+   local fileServeFunction = nil
    if not fileExists then
-      onError(connection, 404, "Not Found")
+      uri.args['code'] = 404
+      fileServeFunction = dofile("httpserver-error.lc")
+   elseif uri.isScript then
+      collectgarbage()
+      fileServeFunction = dofile(uri.file)
    else
-      if uri.isScript then
-         file.close()
-         collectgarbage()
-         dofile(uri.file)(connection, uri.args)
-      else
-         -- Use HTTP/1.0 to ensure client closes connection.
-         connection:send("HTTP/1.0 200 OK\r\nContent-Type: " .. getMimeType(uri.ext) .. "\r\Cache-Control: private, no-store\r\n\r\n")
-         -- Send file in little 128-byte chunks
-         while true do
-            local chunk = file.read(128)
-            if chunk == nil then break end
-            connection:send(chunk)
-         end
-         file.close()
-      end
+      uri.args['file'] = uri.file
+      uri.args['ext'] = uri.ext
+      fileServeFunction = dofile("httpserver-static.lc")
    end
-   collectgarbage()
+   connectionThread = coroutine.create(fileServeFunction)
+   coroutine.resume(connectionThread, connection, uri.args)
 end
 
 local function onReceive(connection, payload)
@@ -107,22 +85,34 @@ local function onReceive(connection, payload)
    local req = parseRequest(payload)
    print("Requested URI: " .. req.uri)
    req.method = validateMethod(req.method)
-   if req.method == nil then onError(connection, 400, "Bad Request")
-   elseif req.method == "GET" then onGet(connection, req.uri)
-   else onError(connection, 501, "Not Implemented") end
-   connection:close()
+   if req.method == "GET" then onGet(connection, req.uri)
+   elseif req.method == nil then dofile("httpserver-static.lc")(conection, {code=400})
+   else dofile("httpserver-static.lc")(conection, {code=501}) end
+end
+
+local function onSent(connection, payload)
+   if coroutine.status(connectionThread) == "dead" then
+      -- We're done sending file.
+      connection:close()
+      connectionThread = nil
+   elseif coroutine.status(connectionThread) == "suspended" then
+      -- Not finished sending file, resume.
+      coroutine.resume(connectionThread)
+   else
+      print ("Fatal error! I did not expect to hit this codepath")
+      connection:close()
+   end
 end
 
 local function handleRequest(connection)
    connection:on("receive", onReceive)
+   connection:on("sent", onSent)
 end
 
 -- Starts web server in the specified port.
-function httpserver.start(port, clientTimeoutInSeconds)
-   s = net.createServer(net.TCP, clientTimeoutInSeconds)
+return function (port)
+   local s = net.createServer(net.TCP, 10) -- 10 seconds client timeout
    s:listen(port, handleRequest)
-   print("nodemcu-httpserver running at " .. wifi.sta.getip() .. ":" ..  port)
+   print("nodemcu-httpserver running at http://" .. wifi.sta.getip() .. ":" ..  port)
    return s
 end
-
-