diff --git a/README.md b/README.md
index 1e882b8..5d01052 100644
--- a/README.md
+++ b/README.md
@@ -1,7 +1,7 @@
# [nodemcu-httpserver](https://github.com/marcoskirsch/nodemcu-httpserver)
A (very) simple web server written in Lua for the ESP8266 running the NodeMCU firmware.
-[From the NodeMCU FAQ](https://nodemcu.readthedocs.org/en/dev/en/faq/#how-do-i-minimise-the-footprint-of-an-application):
+[From the NodeMCU FAQ](https://nodemcu.readthedocs.org/en/dev/en/lua-developer-faq/#how-do-i-minimise-the-footprint-of-an-application):
> If you are trying to implement a user-interface or HTTP webserver in your ESP8266 then
> you are really abusing its intended purpose. When it comes to scoping your ESP8266
diff --git a/TODO.md b/TODO.md
index 6be6681..932cc64 100644
--- a/TODO.md
+++ b/TODO.md
@@ -4,4 +4,3 @@
* Need PUT example. How?
* Rename args.lua to get.lua, so it matches post.lua convention.
* How can I test the whole JSON post thing?
-* BufferedConnection seems too clever. Simplify by removing the buffering and only providing yielding. Worth it? Will make server slower.
diff --git a/http/cars-ferrari.jpg b/http/cars-ferrari.jpg
new file mode 100644
index 0000000..8ac63b7
Binary files /dev/null and b/http/cars-ferrari.jpg differ
diff --git a/http/cars-lambo.jpg b/http/cars-lambo.jpg
new file mode 100644
index 0000000..fb754f0
Binary files /dev/null and b/http/cars-lambo.jpg differ
diff --git a/http/cars-mas.jpg b/http/cars-mas.jpg
new file mode 100644
index 0000000..73ec952
Binary files /dev/null and b/http/cars-mas.jpg differ
diff --git a/http/cars-porsche.jpg b/http/cars-porsche.jpg
new file mode 100644
index 0000000..f78469f
Binary files /dev/null and b/http/cars-porsche.jpg differ
diff --git a/http/cars.html b/http/cars.html
new file mode 100644
index 0000000..6467a8b
--- /dev/null
+++ b/http/cars.html
@@ -0,0 +1,17 @@
+
+
+
+
+ Nice cars
+
+
+
Nice cars!
+
This page loads "large" images of fancy cars. It is meant to serve as a stress test for nodemcu-httpserver.
Nice cars: Stress test, loads 4 "large" images. Makes the chip panic and restart :( (static)
Zipped: File is actually saved as zipped.html.gz. A compressed file! (static but gzipped)
Zipped: Same exact file as served above. Server is smart enough to treat the .gz extension correctly (static but gzipped)
Arguments: Parses arguments passed in the URL and prints them. (Lua)
diff --git a/httpserver-connection.lua b/httpserver-connection.lua
index a5907a3..329e603 100644
--- a/httpserver-connection.lua
+++ b/httpserver-connection.lua
@@ -25,21 +25,39 @@ function BufferedConnection:new(connection)
end
function newInstance:send(payload)
- local l = payload:len()
- if l + self.size > 1024 then
- -- Send what we have buffered so far, not including payload.
- if self:flush() then
- coroutine.yield()
- end
+ local flushthreshold = 1400
+
+ local newsize = self.size + payload:len()
+ while newsize > flushthreshold do
+ --STEP1: cut out piece from payload to complete threshold bytes in table
+ local piecesize = flushthreshold - self.size
+ local piece = payload:sub(1, piecesize)
+ payload = payload:sub(piecesize + 1, -1)
+ --STEP2: insert piece into table
+ table.insert(self.data, piece)
+ self.size = self.size + piecesize --size should be same as flushthreshold
+ --STEP3: flush entire table
+ if self:flush() then
+ coroutine.yield()
+ end
+ --at this point, size should be 0, because the table was just flushed
+ newsize = self.size + payload:len()
end
- if l > 768 then
- -- Payload is big. Send it now rather than buffering it for later.
- self.connection:send(payload)
- coroutine.yield()
- else
- -- Payload is small. Save off payload for later sending.
- table.insert(self.data, payload)
- self.size = self.size + l
+
+ --at this point, whatever is left in payload should be <= flushthreshold
+ local plen = payload:len()
+ if plen == flushthreshold then
+ --case 1: what is left in payload is exactly flushthreshold bytes (boundary case), so flush it
+ table.insert(self.data, payload)
+ self.size = self.size + plen
+ if self:flush() then
+ coroutine.yield()
+ end
+ elseif payload:len() then
+ --case 2: what is left in payload is less than flushthreshold, so just leave it in the table
+ table.insert(self.data, payload)
+ self.size = self.size + plen
+ --else, case 3: nothing left in payload, so do nothing
end
end
diff --git a/httpserver-error.lua b/httpserver-error.lua
index 77f2516..fd9e9ae 100644
--- a/httpserver-error.lua
+++ b/httpserver-error.lua
@@ -4,6 +4,7 @@
return function (connection, req, args)
+ -- @TODO: would be nice to use httpserver-header.lua
local function getHeader(connection, code, errorString, extraHeaders, mimeType)
local header = "HTTP/1.0 " .. code .. " " .. errorString .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\n"
for i, extraHeader in ipairs(extraHeaders) do
@@ -15,9 +16,7 @@ return function (connection, req, args)
print("Error " .. args.code .. ": " .. args.errorString)
args.headers = args.headers or {}
- local html = getHeader(connection, args.code, args.errorString, args.headers, "text/html")
- html = html .. "" .. args.code .. " - " .. args.errorString .. "
\r\n")
end
diff --git a/httpserver-header.lua b/httpserver-header.lua
index 9f8f7bc..d218722 100644
--- a/httpserver-header.lua
+++ b/httpserver-header.lua
@@ -19,14 +19,11 @@ return function (connection, code, extension, isGzipped)
local mimeType = getMimeType(extension)
- local header = "HTTP/1.0 " .. code .. " " .. getHTTPStatusString(code) .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\nnCache-Control: private, no-store\r\n"
+ connection:send("HTTP/1.0 " .. code .. " " .. getHTTPStatusString(code) .. "\r\nServer: nodemcu-httpserver\r\nContent-Type: " .. mimeType .. "\r\nnCache-Control: private, no-store\r\n")
if isGzipped then
- header = header .. "Cache-Control: max-age=2592000\r\n"
- header = header .. "Content-Encoding: gzip\r\n"
+ connection:send("Cache-Control: max-age=2592000\r\nContent-Encoding: gzip\r\n")
end
- header = header .. "Connection: close\r\n\r\n"
- connection:send(header)
- header = nil
+ connection:send("Connection: close\r\n\r\n")
end
diff --git a/httpserver-static.lua b/httpserver-static.lua
index 9140b86..178b17b 100644
--- a/httpserver-static.lua
+++ b/httpserver-static.lua
@@ -4,14 +4,18 @@
return function (connection, req, args)
--print("Begin sending:", args.file)
+ --print("node.heap(): ", node.heap())
dofile("httpserver-header.lc")(connection, 200, args.ext, args.isGzipped)
-- Send file in little chunks
local continue = true
local size = file.list()[args.file]
local bytesSent = 0
- local chunkSize = 1024 -- @TODO: can chunkSize be larger?
+ -- Chunks larger than 1024 don't work.
+ -- https://github.com/nodemcu/nodemcu-firmware/issues/1075
+ local chunkSize = 1024
while continue do
collectgarbage()
+
-- NodeMCU file API lets you open 1 file at a time.
-- So we need to open, seek, close each time in order
-- to support multiple simultaneous clients.
diff --git a/httpserver.lua b/httpserver.lua
index 9e1dd87..29ac0a2 100644
--- a/httpserver.lua
+++ b/httpserver.lua
@@ -9,17 +9,17 @@ return function (port)
port,
function (connection)
- -- 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.
+ -- This variable holds the thread (actually a Lua coroutine) used for sending data back to the user.
+ -- We do it in a separate thread because we need to send in little chunks and wait for the onSent event
+ -- before we can send more, or we risk overflowing the mcu's buffer.
local connectionThread
local allowStatic = {GET=true, HEAD=true, POST=false, PUT=false, DELETE=false, TRACE=false, OPTIONS=false, CONNECT=false, PATCH=false}
local function startServing(fileServeFunction, connection, req, args)
-
connectionThread = coroutine.create(function(fileServeFunction, bufferedConnection, req, args)
fileServeFunction(bufferedConnection, req, args)
+ -- The bufferedConnection may still hold some data that hasn't been sent. Flush it before closing.
if not bufferedConnection:flush() then
connection:close()
connectionThread = nil
@@ -32,17 +32,14 @@ return function (port)
if not status then
print("Error: ", err)
end
-
end
- local function onRequest(connection, req)
+ local function handleRequest(connection, req)
collectgarbage()
local method = req.method
local uri = req.uri
local fileServeFunction = nil
- --print("Method: " .. method);
-
if #(uri.file) > 32 then
-- nodemcu-firmware cannot handle long filenames.
uri.args = {code = 400, errorString = "Bad Request"}
@@ -105,14 +102,14 @@ return function (port)
-- parse payload and decide what to serve.
local req = dofile("httpserver-request.lc")(payload)
- print("Requested URI: " .. req.request)
+ print(req.method .. ": " .. req.request)
if conf.auth.enabled then
auth = dofile("httpserver-basicauth.lc")
user = auth.authenticate(payload) -- authenticate returns nil on failed auth
end
if user and req.methodIsValid and (req.method == "GET" or req.method == "POST" or req.method == "PUT") then
- onRequest(connection, req)
+ handleRequest(connection, req)
else
local args = {}
local fileServeFunction = dofile("httpserver-error.lc")
@@ -145,14 +142,16 @@ return function (port)
end
end
- connection:on("receive", onReceive)
- connection:on("sent", onSent)
- connection:on("disconnection",function(c)
+ local function onDisconnect(connection, payload)
if connectionThread then
connectionThread = nil
collectgarbage()
end
- end)
+ end
+
+ connection:on("receive", onReceive)
+ connection:on("sent", onSent)
+ connection:on("disconnection", onDisconnect)
end
)