从零开始做一个智能排插#4:HTTP服务器

一不小心又拖了大半个月。
这次我们将搭建一个HTTP服务器,然后通过访问网页来控制LED灯的开关。

惯例先放上各种链接

nodemcu的wifi模块

wifi模块的AP模式

ESP8266是一个wifi模块,所以核心当然就是它的wifi功能。
nodemcu可以设置4种wifi模式,分别是STA模式wifi.STATION,AP模式wifi.SOFTAP,STA+AP模式wifi.STATIONAP,还有无模式wifi.NULLMODE

我们先介绍AP模式,它可以开放一个热点让其他设备连接上ESP8266。

1
2
3
4
5
6
cfg={}                      -- 声明一个cfg变量存放AP模式的配置
cfg.ssid="myssid" -- 配置AP模式的ssid为"myssid"
cfg.pwd="mypassword" -- 配置AP模式的密码为"mypassword"
cfg.auth=wifi.WPA2_PSK -- 配置AP模式的安全类型为WPA2_PSK,不设置密码可以使用wifi.OPEN
wifi.setmode(wifi.SOFTAP) -- 设置wifi模式为wifi.SOFTAP
wifi.ap.config(cfg) -- 使用cfg配置AP模式

设置好之后我们就可以在WLAN列表里找到我们的热点了(不知道为什么我用电脑连不上,只能通过手机连接,好像很多人都遇到了这个问题,不过不影响我们调试,暂时先不管)。
通过wifi.ap.config()还可以设置AP模式的加密模式之类的,具体细节可以自己看官方文档。

wifi模块的STA模式

STA模式可以让ESP8266连接到其他热点

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
station_cfg={}                          -- 声明一个station_cfg变量存放STA模式的配置
station_cfg.ssid="NODE-AABBCC" -- 待连接热点的ssid
station_cfg.pwd="password" -- 待连接热点的密码
wifi.setmode(wifi.STATION) -- 设置wifi模式为wifi.STATION

ip = wifi.sta.getip() -- 声明一个变量存放STA模式的ip地址
if not(ip) then -- 如果没有获取到ip地址
wifi.sta.config(station_cfg) -- 使用station_cfg配置AP模式
print("connecting...") -- 打印connecting...
tmr.alarm(1, 1000, 1, function() -- 设置定时器每秒一次检测
ip = wifi.sta.getip() -- 获取ip地址
if ip then -- 获取到ip则打印ip地址
print(ip)
tmr.unregister(1) -- 停止定时器
end
end)
end

连接到热点时就会输出ip地址

其他细节可以看官方文档里的wifi.sta.config()

如果想要同时设置AP和STA模式可以使用wifi.setmode(wifi.STATIONAP)开启STA+AP模式

一个简单的HTTP服务器

HTTP请求

首先是官网上的示例

1
2
3
4
5
6
7
8
-- a simple http server
srv=net.createServer(net.TCP) -- 创建一个基于TCP协议的服务
srv:listen(80,function(conn) -- 监听80端口
conn:on("receive",function(conn,payload) -- 当端口接收到请求时
print(payload) -- 打印请求消息
conn:send("<h1> Hello, NodeMcu.</h1>") -- 发送数据
end)
end)

先把程序上传到nodemcu里,然后在串口工具里输入wifi.setmode(wifi.STATIONAP)开启AP模式。
手机连接到热点后我们在串口工具里输入=wifi.ap.getip()就可以获取到nodemcu服务器的ip地址,在手机浏览器中输入这个ip地址我们就能访问当nodemcu服务器。


之后串口工具中将会输出一大串字符,这是来自我们手机浏览器上一段的请求消息,它包含了请求行、请求头、空行和报文主体
其中最重要的信息是第一行的:

1
GET / HTTP/1.1

这一行包含了3个信息,第一个GET表示浏览器使用GET方式发出请求,第二个/表示浏览器想访问网站的根文件夹,第三个HTTP/1.1表示浏览器使用的是 HTTP 1.1 协议。

首先是请求方法GET,HTTP协议中定义了各种请求方法,其中GET表示浏览器想要获取当前页面信息,于是示例中通过conn:send("<h1> Hello, NodeMcu.</h1>")返回了页面信息,浏览器收到这段信息就会显示出来。不过其实这个没什么关系,毕竟是协议,也就是说这不是强制性的,你完全可以按照你的方式来写服务器,用自己的方式处理各种请求。

然后是URL/,我们可以这样理解,一个网站就是电脑里的一个文件夹,URL就是我们电脑文件夹的路径,/就代表这个文件夹的根目录,就是指这个文件夹本身,假设这个文件夹里有一个文件叫index.html,那么我们就可以用/index.html来表示。不过这个在示例中并没有太大作用,我们之后再详细解释。

最后是协议版本HTTP/1.1,其实我们写HTTP服务器的本质就是处理浏览器发送过来的请求。10年前我们还在用XP,现在我们已经用上了win10,谁敢保证我们10年后我们处理请求的方式跟现在一样呢?所以我们就用这个协议版本加以区分,表示服务器应该按 HTTP 1.1 的方式来处理请求。

HTTP响应

浏览器发送过来一段请求,我们处理完之后理所当然的需要返回一个响应消息告诉浏览器我们已经处理完了这段请求。
在官网示例中conn:send("<h1> Hello, NodeMcu.</h1>")就完成了这个操作,但这并不完整,一个完整的响应应该包含状态行、响应头、空行和报文主体。
我们修改一下示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
srv = net.createServer(net.TCP)
srv:listen(80, function(conn)
conn:on("receive", function(conn, payload)
print(payload)

body = "<h1> Hello, NodeMcu.</h1>" -- 报文主体
status = "HTTP/1.1 200 OK" .. "\r\n" -- 状态行
type_ = "Content-Type: text/html" .. "\r\n" -- 响应头中的Content-Type
length = "Content-Length: " .. string.len(body) .. "\r\n" -- 响应头中的Content-Length

conn:send(status .. type_ .. length .. "\r\n" .. body) -- 发送数据
end)
end)

浏览器正常是不会在页面中显示状态行和响应头的,所以我们要使用浏览器的开发者工具查看。

这里有个问题,就是刚刚说了,好像很多人的电脑(尤其windows系统)都连不上nodemcu的热点,所以当我们无法接连nodemcu热点时,可以使用STA+AP模式,先让nodemcu和电脑连接到同一个热点,然后用电脑访问nodemcu在STA模式下的ip地址

然后我们就可以用浏览器的开发者工具查看完整的响应消息了

解析HTTP请求

现在我们大致明白HTTP协议是怎么运作的了,但是我们究竟要怎么通过HTTP控制nodemcu呢?这个时候就要把HTTP请求中包含的信息解析出来,然后让nodemcu执行对应的操作。

在这里,我们可以通过解析请求中的URL来判断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
-- 解析请求行
function parseRequest(payload)
local req = {} -- 声明一个req变量用于存储URL信息
local _GET = {} -- 声明一个_GET变量用于存储URL参数

-- 通过正则获取请求行, 格式例如 GET /?a=1 HTTP/1.1
local _, _, method, path, query = string.find(payload, "([A-Z]+) (.+)?(.+) HTTP")

-- 如果获取失败
if(method == nil) then
-- 获取不含参数的请求行, 格式例如 GET /index.html HTTP/1.1
_, _, method, path = string.find(payload, "([A-Z]+) (.+) HTTP")
end

-- 如果请求行包含参数
if(query ~= nil) then
-- 提取参数, 格式例如 a=1&b=&_C_=3
for k, v in string.gmatch(query, "([^&#]+)=([^&#]*)&*") do
_GET[k] = v
end
end

req.method = method -- 请求方法
req.query = _GET -- 参数
req.path = path -- 路径

return req -- 返回req
end

关于lua的正则可以查看Lua 5.3参考手册的6.4.1章匹配模式

例如当我们访问的url为http://192.168.0.103/config?led=on,将会得到如下req

1
2
3
4
5
6
7
req = {
path = "/config",
method = "GET",
query = {
led = "on"
}
}

通过判断req.qurey中的参数,我们就可以让nodemcu执行我们想要的操作。

以上就是一个简单的HTTP服务器。

通过网页控制LED开关

有了刚才的函数,我们只需要解析URL中的参数就能控制LED灯了。

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
-- 使用pin4的指示灯
LED = 4
gpio.mode(LED, gpio.OUTPUT)

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 -- 获取led参数
if(led_status) then -- 存在led参数
gpio.write(LED, led_status == "off" and 1 or 0) -- 如果led参数为off则置高电平,否则置低电平
end

-- 页面中添加两个LED开关按钮
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)

完美。

开头的链接中提供了一个别人写好的HTTP框架,不嫌看源码麻烦的话可以试试(总比自己写方便)。
至于关于怎么写网页可以看这个HTML 教程