news 2026/9/28 20:56:58

NodeMCU httpserver 模块实战指南:用 Lua 在 ESP8266 上实现 HTTP/1.1 服务器

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
NodeMCU httpserver 模块实战指南:用 Lua 在 ESP8266 上实现 HTTP/1.1 服务器
  • 物联网
  • 嵌入式

【免费下载链接】nodemcu-firmware

Lua based interactive firmware for ESP8266, ESP8285 and ESP32

项目地址:https://gitcode.com/gh_mirrors/no/nodemcu-firmware
点击查看免费下载

导读

NodeMCU 固件内置的net模块只提供最底层的 TCP 能力,直接用它手写 HTTP 协议解析既繁琐又容易踩内存的坑。本文讲解的httpserverLua 模块(源码位于 lua_modules/http/httpserver.lua,官方文档见 docs/lua-modules/httpserver.md)为 NodeMCU 提供了一套基于回调的 HTTP/1.1 服务器实现,你只需调用httpserver.createServer(port, handler)并填写请求/响应回调,就能在 ESP8266 上快速搭建一个可用的 Web 服务。读完本文,你将掌握该模块的完整 API(req请求对象与res响应对象)、正确的回调编写方式、一个可直接运行的 Hello World 示例,以及其底层的请求解析、chunked 分块传输和发送队列实现原理。

一、模块定位:纯 Lua 实现的 HTTP/1.1 服务器

httpserver是一个完全用 Lua 编写的 HTTP 服务器模块,最早由 Vladimir Dronnikov(dvv)于 2015-01-19 贡献,目前也由该作者维护。它没有依赖固件内置的 C 模块,而是直接构建在netTCP 服务器与fifosock发送队列之上,通过回调函数把"收到请求头""收到请求体""连接断开"等事件暴露给业务代码。

在 NodeMCU 官方文档中,该模块被收录在Lua Modules一节,与固件内置的 C 模块(如http客户端)是分开维护的:它是可选的纯 Lua 模块,需要把 httpserver.lua 上传到设备文件系统(SPIFFS 或 LFS)后使用,而不是编译进固件。

二、模块的加载与释放

使用前先通过require加载模块:

httpserver = require("httpserver")

需要释放模块(例如更新模块代码、释放内存)时,按如下顺序操作,确保彻底卸载:

httpserver = nil package.loaded["httpserver"] = nil

先清空全局变量引用,再把package.loaded中的缓存条目移除,这样下次require才会重新加载最新版本的模块文件。

三、启动服务器:httpserver.createServer()

createServer是模块暴露的唯一下层入口(模块顶层的http表中只定义了createServer这一个字段),调用后会立即开始监听端口并等待连接。

语法与参数

httpserver.createServer(port, handler(req, res))
参数说明
portHTTP 服务器监听的端口号。绝大多数 HTTP 服务器监听 80 端口(ESP8266 上如果同时开了其他服务,要注意端口冲突)。
handler回调函数,每当收到 HTTP 请求时被调用。它接收两个参数:req(请求对象)与res(响应对象),具体字段与方法见下文。

返回值

返回net.server子模块(即net.createServer创建出来的服务器对象)。也就是说,createServer的返回值可以继续调用net模块的服务器方法,比如srv:close()手动关闭服务。

单实例限制与生命周期

从源码 httpserver.lua 中可以看到,createServer内部有一个关键设计:

-- NB: only one server at a time if srv then srv:close() end srv = net.createServer(net.TCP, 15) srv:listen(port, http_handler(handler)) return srv
  • 同时只允许一个服务器实例:模块用局部变量srv保存上一次创建的服务器,再次调用createServer时会先把旧的服务器close()掉。所以重复调用createServer不会造成端口被占用,但旧服务会被静默关闭,多端口监听需要自行扩展。
  • 服务器由net.createServer(net.TCP, 15)创建,其中的超时参数会传递给底层 TCP 服务器(net.createServer的超时参数取值范围为 1~28 800 秒,默认 30 秒,详见 docs/modules/net.md),意味着长时间不活跃的客户端连接会被底层自动断开。

四、req 请求对象

handler的第一个参数req是一个普通 Lua table,其中既有数据字段,也有可覆盖的回调字段:

字段类型/用途说明
connnet.socket子模块底层 TCP 连接对象。不要在这个对象上调用:on或:send,否则会破坏模块内部的解析状态机与发送队列;所有响应都必须通过res对象完成。
method字符串请求使用的方法,例如GET、POST。
url字符串请求的 URL 路径。
onheader回调函数请求头解析完成时被调用,函数签名onheader(self, name, value)。name永远是小写形式(模块会把请求头名称统一转小写)。
ondata回调函数请求体数据到达时被调用,函数签名ondata(self, chunk)。当全部请求体接收完毕时,会额外调用一次chunk为nil的收尾回调。

onheader:根据请求头决定解析策略

onheader在每一个请求头可用时立即触发(头是逐行解析的,解析到哪行回调到哪行)。典型用途是根据content-type选择请求体解析器:

req.onheader = function(self, name, value) print("+H", name, value) -- 例如根据 content-type 决定 body 的解析方式 -- if name == "content-type" then -- if value == "application/json" then -- req.ondata = function(self, chunk) ... end -- elseif value == "application/x-www-form-urlencoded" then -- req.ondata = function(self, chunk) ... end -- end -- end end

ondata:处理请求体

ondata在请求体数据分块到达时被反复调用;注意它同样遵循 NodeMCU 网络模块"最后一个 chunk 为nil"的约定,chunk == nil表示整个请求体已经收完,此时应当发送响应。示例 http-example.lua 正是这样判断"请求体结束 → 回写响应"的:

req.ondata = function(self, chunk) print("+B", chunk and #chunk, node.heap()) if not chunk then -- 请求体收完,发送响应 res:send(nil, 200) res:send_header("Connection", "close") res:send("Hello, world!\n") res:finish() end end

关于conn字段的使用提醒:官方文档特别强调DO NOT在req.conn上调用:on或:send。因为模块内部已经为这条连接注册了receive/disconnection/sent事件回调并用fifosock包装了发送通道,直接操作conn会绕过解析器与队列,导致响应乱序甚至丢数据。

五、res 响应对象

handler的第二个参数res提供三个方法,用于向客户端写回 HTTP 响应:

res:send(data, [response_code])

发送数据到客户端。

res:send(data, [response_code])
  • data:要发送的数据,可以为nil(此时只发送状态行与响应头,不发送 body 数据)。
  • response_code:HTTP 响应码,如200(默认)或404。注意:多次调用send时只有第一次传入的响应码会生效,后续传入的码都会被忽略。

res:send_header(header_name, header_data)

发送 HTTP 响应头。

res:send_header(header_name, header_data)
  • 必须在响应体开始发送之前调用。源码中send一旦发送了 body 数据就会把self.send_header置为nil,之后send_header方法将不再可用(调用会报错)。
  • 模块内部在发送首个send时会自动附带Transfer-Encoding: chunked头(详见下文"chunked 分块传输"),你只需发送自定义头。

res:finish([data[, response_code]])

结束并关闭连接。

res:finish([data[, response_code]])
  • data:可选,结束时一并发送的数据。
  • response_code:可选,响应码,规则与send相同(只有首次生效)。
  • 调用finish后模块会写入 chunked 编码的结束标记0\r\n\r\n,并在全部数据真正发送完毕后关闭底层连接、清理事件回调。

一个最简单的响应可以只写一行:

res:finish("Hello, world!")

finish内部会先走一遍send,因此res:finish("Salut, monde!")这种写法等价于"发送数据 + 结束连接",适合快速返回小体积响应。

六、完整可运行示例

仓库自带的 http-example.lua 是一个完整的 Hello World 服务器。把它上传到设备后,运行下面的代码即可启动:

require("httpserver").createServer(80, function(req, res) -- 分析请求方法与 URL print("+R", req.method, req.url, node.heap()) -- 注册请求头回调(如果有请求头) req.onheader = function(self, name, value) -- luacheck: ignore print("+H", name, value) -- 可根据 content-type 选择 body 解析方式 end -- 注册请求体回调(如果有请求体) req.ondata = function(self, chunk) -- luacheck: ignore print("+B", chunk and #chunk, node.heap()) if not chunk then -- 请求体收完,回写响应 res:send(nil, 200) res:send_header("Connection", "close") res:send("Hello, world!\n") res:finish() end end -- 或者不等待请求体,直接返回: --res:finish("Hello, world!") --res:finish("Salut, monde!") end)

运行流程梳理:

  1. createServer(80, handler)创建 TCP 服务器并监听 80 端口;
  2. 浏览器/客户端发起请求,TCP 连接建立,模块开始逐行解析;
  3. 解析到请求行(如GET / HTTP/1.1)后,构造req、res对象并调用handler(req, res),此时req.method、req.url已可用;
  4. 每解析到一个请求头,触发一次req.onheader;
  5. 请求头解析完毕,后续接收到的数据全部作为请求体交给req.ondata,直到chunk == nil表示收尾;
  6. 业务代码在收尾回调中调用res:send/res:send_header/res:finish回写响应,模块负责把数据按 chunked 编码发出并关闭连接。

七、底层实现剖析

7.1 请求解析状态机

http_handler(handler)(lua_modules/http/httpserver.lua)把一条 TCP 连接包装成一个逐行解析的状态机:

  • 请求行解析:用模式^([A-Z]+) (.-) HTTP/1.1$从第一行提取method与url。从源码注释NB: just version 1.1 assumed可以推断,该模块假定客户端都使用 HTTP/1.1 请求行;如果请求行不是这个格式(例如HTTP/1.0),将不会被识别为合法请求。
  • 请求头解析:用模式^([%w-]+):%s*(.+)解析名称: 值形式,并把名称lower()转小写后交给onheader。
  • 请求头结束判定:遇到空行表示头部结束。此时模块重新挂载receive回调为ondata,并把当前缓冲区剩余部分作为请求体的第一个 chunk 喂给ondata——这是典型的"边接收边解析"做法,避免数据滞留在 Lua 字符串拼接中。

7.2 Content-Length 与请求体结束信号

解析头部期间,模块会特别关注两个头:

  • content-length:记录到局部变量cnt_len,用于判断请求体何时接收完毕;
  • expect: 100-continue:模块会自动回写HTTP/1.1 100 Continue\r\n,配合客户端的分段上传协议。

请求体处理逻辑在ondata内部:每收到一个 chunk,累加body_len,当body_len >= cnt_len时再调用一次req:ondata()(不带参数,等价于chunk == nil),通知业务代码请求体结束。因此,即使客户端没有发content-length头,只要连接关闭或数据收尾,你依然会在ondata中收到一次nil收尾回调。

7.3 chunked 分块传输编码

res:send在首次发送时自动输出:

HTTP/1.1 200 OK\r\n Transfer-Encoding: chunked\r\n ...(你通过 send_header 添加的头)... \r\n

随后每个data都以%X\r\n形式输出十六进制的数据长度,后跟数据与\r\n;res:finish最后输出0\r\n\r\n作为 chunked 编码的终止标记(httpserver.lua 中的csend("0\r\n\r\n"))。选择 chunked 编码是为了不依赖Content-Length头——在 ESP8266 这种小内存设备上,业务代码可以一边生成数据一边发送,无需预先知道总长度。

需要留意的实现限制(源码中均有 TODO 注释佐证):

  • 状态行固定输出"HTTP/1.1 <code> OK\r\n",没有真实的 HTTP 状态码/名称表,404 等状态也会附带OK文本;
  • 不会自动发送Server:、Date:等标准响应头;
  • 响应体一旦开始发送,不允许再追加响应头。

7.4 fifosock 发送队列

模块用(require "fifosock").wrap(conn)把底层 socket 的send包装成csend(fifosock.lua)。fifosock是一个两段式 FIFO 发送队列:它会合并小字符串以减少 TCP 包数量、把大字符串切块,并在 socket 的sent事件驱动下按序发送,同时支持把函数排入队列作为"发送完成回调"。

这带来两个实际影响:

  1. res:send/res:finish只是把数据写进队列,真正的网络发送由sent事件异步驱动,所以返回后数据不一定已到达客户端;
  2. httpserver.lua中的cfini通过csend(function() conn:close() ... end)的方式,确保所有排队的响应数据都发送完毕后才关闭连接,避免响应被截断。同时,由于fifosock会在 Lua registry 中形成 socket 与包装器的循环引用(详见 docs/lua-modules/fifosock.md),模块在断开回调ondisconnect中主动清空三个事件回调并调用collectgarbage("collect")来回收内存——这也是为什么你不应该在req.conn上自行注册回调的原因之一。

7.5 内存与连接清理

每次连接断开时,ondisconnect会执行:

connection:on("receive", nil) connection:on("disconnection", nil) connection:on("sent", nil) collectgarbage("collect")

把三个事件回调全部摘除,再主动触发一次完整 GC。配合net.createServer的 15 秒超时参数(httpserver.lua 中传入),长时间无活动的连接会被底层自动回收。这保证了在持续请求的场景下,模块不会因为积累废弃连接和 Lua 字符串缓冲区而耗尽 ESP8266 稀缺的 RAM。你可以在ondata/onheader回调中用node.heap()观察剩余堆内存,验证内存回收效果(示例代码中已有该打印)。

八、实践要点与注意事项

  1. 端口选择:通常监听 80 端口,方便浏览器直接访问;若设备上同时运行了固件内置http模块或其他服务,注意避免端口冲突。
  2. 不要直接操作req.conn:conn仅供读取信息,:on/:send一律通过res完成,否则会破坏解析状态机与发送队列。
  3. 注意请求行格式:模块按HTTP/1.1请求行解析,标准浏览器与 curl 均满足该格式;自研客户端务必使用 HTTP/1.1。
  4. 响应头必须在 body 之前发送:send_header在第一次send之后即变为nil,响应码也只有第一次生效。
  5. 善用finish:如果响应体可以在请求体到达前就生成(如静态内容、简单的状态页),直接调用res:finish(data, code)即可,不必等待ondata的收尾回调。
  6. 内存敏感场景:ESP8266 可用 RAM 有限,建议在回调中监控node.heap();返回大响应时尽量分块send,让fifosock队列异步发送,避免一次性构造超长字符串。

九、延伸阅读

  • 模块官方文档(本指南的 API 依据):docs/lua-modules/httpserver.md
  • 模块实现源码:lua_modules/http/httpserver.lua
  • 完整示例:lua_modules/http/http-example.lua
  • 底层依赖 fifosock 模块文档与源码:docs/lua-modules/fifosock.md、lua_modules/fifo/fifosock.lua
  • 底层 TCP 服务器net.createServer的 C 实现与文档:app/modules/net.c、docs/modules/net.md
  • 物联网
  • 嵌入式

【免费下载链接】nodemcu-firmware

Lua based interactive firmware for ESP8266, ESP8285 and ESP32

项目地址:https://gitcode.com/gh_mirrors/no/nodemcu-firmware
点击查看免费下载

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 20:56:38

细胞分析仪专用显示屏解决方案:医疗级防护、精准显示、稳定运行

细胞分析仪用在医院检验科、第三方检测机构、科研实验室&#xff0c;每天处理大量样本&#xff0c;屏幕是操作人员接触最频繁的部件。这些年经手的项目里&#xff0c;用屏遇到的问题不少&#xff0c;今天总结一下&#xff0c;纯干货分享。 第一个问题&#xff1a;色彩不准&…

作者头像 李华
网站建设 2026/9/28 20:56:16

2026年了,想入行AI领域?这份“AI证书”考取指南请收好

嘿&#xff0c;朋友&#xff01;是不是感觉2026年的职场&#xff0c;到处都在聊AI、大数据、大模型&#xff1f;看得人心里痒痒的&#xff0c;也想搭上这趟时代的快车&#xff1f;但打开招聘软件一看&#xff0c;心凉了半截——岗位要求上的技能树点得密密麻麻&#xff0c;没有…

作者头像 李华
网站建设 2026/9/28 20:54:17

测试人转型AI测试开发:用LangChain搭建UI自动化脚本生成Agent

测试行业这两年最明显的变化&#xff0c;不是工具变多了&#xff0c;而是招聘JD里的要求变了。以前打开岗位描述&#xff0c;清一色写着"熟悉Selenium、Appium、Postman&#xff0c;有接口自动化经验优先"&#xff1b;现在再刷&#xff0c;越来越多的岗位开始加一条&…

作者头像 李华
网站建设 2026/9/28 20:49:44

生产级智能体平台落地指南:任务编排、工具管理与运行监控实践

做生产级智能体平台&#xff0c;说白了就是三件事&#xff1a;任务编排、工具管理、运行监控。我见过太多团队冲着“大模型”去搭平台&#xff0c;最后都烂在这三件事上——业务没跑几个&#xff0c;代码全堆在链式调用里&#xff1b;工具越接越多&#xff0c;密钥散落在各个服…

作者头像 李华