跳转至

02 HTTP

作者:马梦阳

一、HTTP 概述

1.1 什么是 HTTP?

HTTP 全称为 HyperText Transfer Protocol,中文即“超文本传输协议”。它是一种应用层协议,采用标准的请求-响应模型,通常运行在 TCP 之上,规定了客户端可以向服务器发送何种消息以及预期得到何种响应,常用于分布式、协作式和超媒体信息系统。

1.2 什么是 HTTPS?

HTTPS 全称为 HyperText Transfer Protocol Secure,中文即“超文本传输安全协议”。它并非一种新的协议,而是在 HTTP 之下插入了一层 SSL/TLS 安全通道,同样运行在 TCP 之上。通过加密与身份认证,HTTPS 确保客户端与服务器之间交换的消息不被窃听或篡改,从而为分布式、协作式和超媒体信息系统提供了保密性、完整性与身份可验证的通信基础。

1.3 HTTP 的工作原理(请求-响应模型)

HTTP 采用请求-响应模型:客户端(如浏览器)向服务器发送请求,服务器以状态码和对应资源的表示(如 HTML 字节流)作出响应。

1.3.1 HTTP 请求报文

一个完整的 HTTP 请求包含请求行、请求头、空行、请求体四部分:

1. Request line(请求行)

  • Method:请求方式,如 GETPOST
  • Request-URL:需要访问的目标路径,比如 /index.html
  • HTTP-Version:HTTP 协议版本号,比如 HTTP/1.1

2. Header Lines(请求头)

由键值对组成,每行一对。请求头包含关于客户端环境和请求正文的重要信息。常见的请求头有:

  • Host:指定请求的服务器的域名和端口号(HTTP/1.1 必需字段)。
  • User-Agent:包含发起请求的应用程序信息(浏览器类型、操作系统等)。
  • Accept:告知服务器客户端能够处理哪些类型的媒体资源(如 text/html, application/json)。
  • Content-Type:(用于有 Body 的请求)请求体的媒体类型(如 application/json, application/x-www-form-urlencoded)。
  • Content-Length:(用于有 Body 的请求)请求体的长度(字节)。

3. Blank line(空行)

就是一个空行,用来分隔头部和正文,告诉服务器:“头部结束,下面是正文了”。

4. Entity Body(请求体)

可选部分,主要用于 POSTPUT 等需要向服务器发送数据的请求。

  • 内容格式由 Content-Type 头指定。
  • 常见内容:表单数据(user=admin&pass=123)、JSON 字符串({"user":"admin"})、文件数据等。

请求报文示例如下:

1.3.2 HTTP 响应报文

一个完整的 HTTP 响应报文与请求报文几乎一一对应,也是四部分:

1. Status Line(状态行)

  • HTTP-Version:与请求消息中的版本相匹配,如 HTTP/1.1
  • Status Code:三位数字,标识请求的处理结果,如 200404
  • Reason-Phrase:状态码的简短文字描述,如 OKNot Found

2. Header Lines(响应头)

一样每行是一个“键值对”,响应头包含关于响应的附加信息。常见的响应头有:

  • Server:包含处理请求的服务器软件信息。
  • Date:响应生成的日期和时间。
  • Content-Type:响应体的媒体类型(如 text/html; charset=UTF-8)。
  • Content-Length:响应体的长度(字节)。
  • Content-Encoding:响应体使用的编码(如 gzip),用于压缩。
  • Cache-Control:指示响应内容应如何被缓存。

3. Blank Line(空行)

响应头和响应体之间的分隔符,表示响应头的结束。

4. Response Body(响应体)

可选部分,包含服务器返回的实际资源内容。

  • 内容格式由 Content-Type 头指定。
  • 常见内容:HTML 文档、JSON 数据、图片、CSS、JavaScript 文件等。

响应报文示例如下:

1.4 HTTP 请求方法

HTTP 客户端发出请求,告知服务端需要执行不同类型的请求命令,这些命令被称为 HTTP 方法。

1.4.1 常见 HTTP 请求方法

下表展示的是常见的 HTTP 请求方法:

HTTP请求方法
描述
GET
向服务器发起请求以获取指定资源。GET请求只用于获取数据,不会产生任何“副作用”(如修改数据)。
POST
向指定资源提交数据,请求服务器进行处理(如提交表单或上传文件)。数据包含在请求体中。POST请求可能会导致新资源的创建和/或现有资源的修改。
PUT
替换目标资源的所有当前表示。客户端提供完整的更新后的资源。如果资源不存在,PUT可能会创建一个新资源。(如更新用户的完整个人信息)。
PATCH
对资源进行部分更新。客户端只需要提供需要修改的资源,而不是整个资源。(如只更新用户的手机号码或邮箱)。
DELETE
请求服务器删除指定资源(如删除一篇文章或一个用户)。
HEAD
与GET方法相同,但服务器只返回响应头,不返回响应体。用于在不获取整个资源的情况下,检查资源的存在性、获取元数据(如最后修改时间)。
OPTIONS
请求服务器告知其支持哪些HTTP方法,或者用于检查服务器的性能(预检请求)。
CONNECT
建立一个到由目标资源标识的服务器的隧道。通常用于通过代理服务器建立SSL加密隧道(HTTPS)。
TRACE
沿着到目标资源的路径执行一个消息环回测试。主要用于诊断和测试,服务器会在响应体中返回它收到的请求内容

1.4.2 各版本定义的 HTTP 请求方法

HTTP 标准目前有 HTTP/1.0HTTP/1.1HTTP/2HTTP/3 四个版本,介绍如下:

HTTP/1.0

HTTP/1.0 定义了以下三种请求方法:

  • GET:请求指定的资源。
  • POST:提交数据以处理请求。
  • HEAD:请求资源的响应头信息。

HTTP/1.1

HTTP/1.1 引入了更多的请求方法:

  • GET:请求指定的资源。
  • POST:提交数据以处理请求。
  • HEAD:请求资源的响应头信息。
  • PUT:上传文件或者更新资源。
  • DELETE:删除指定的资源。
  • OPTIONS:请求获取服务器支持的请求方法。
  • TRACE:回显服务器收到的请求,主要用于诊断。
  • CONNECT:建立一个隧道用于代理服务器的通信,通常用于 HTTPS。

HTTP/2

HTTP/2 基本上沿用了 HTTP/1.1 的方法,但对协议进行了优化,提高了传输效率和速度。HTTP/2 也引入了新的特性,如多路复用、头部压缩和服务器推送等。

HTTP/3

HTTP/3 基于 QUIC 协议实现,继续使用 HTTP/2 的方法。HTTP/3 主要改进了传输层,使用 UDP 代替 TCP 以提高传输速度和可靠性。

1.5 HTTP 状态码

HTTP 状态码由三个十进制数字组成,第一个十进制数字定义了状态码的类型,响应分为五类:

类别
范围
示例
含义
1xx
信息性
100 Continue
请求已接收,继续处理
2xx
成功
200 OK
请求成功
3xx
重定向
301 Moved Permanently
资源已永久移动
4xx
客户端错误
404 Not Found
资源未找到
5xx
服务器错误
500 Internal Server Error
服务器内部错误

二、http 与 httpplus 库的介绍

注意:合宙 LuatOS 仅支持 HTTP/1.0HTTP/1.1

HTTP 在如下的场景,开发成本是最低的:

1. 终端和云端交换文件,不用开发复杂的分包组包协议;

2. 终端从云端下载各种参数,或者终端向云端提交各种参数, 不用设计报文协议,用 http 的参数即可,非常方便。

LuatOS 提供了 http 核心库 和 httpplus 扩展库 实现了 http 客户端。

http 核心库 和 httpplus 扩展库 的区别如下:

**区别项**
**http核心库**
**httpplus扩展库**
文件上传
文件最大64KB
只要内存够用,文件大小不限
文件下载
支持,只要文件系统空间够用,文件大小不限
不支持
http header的key:value的限制
所有header的value总长度不能超过4KB,单个header的value长度不能超过1KB
只要内存够用,header长度不限
鉴权URL自动识别
不支持
支持
接收到的body数据存储支持zbuff
不支持
支持,可以直接传输给uart等库
接收到的body数据存储到内存中
最大支持32KB
只要内存够用,大小不限
chunk编码
支持
不支持

如果你想要详细了解 http 核心库,请 点击此处

如果你想要详细了解 httpplus 扩展库,请 点击此处

三、演示功能概述

在使用示例代码测试时,如果遇到测试域名请求不成功的情况,可以向合宙工作人员进行反馈。

1、http_app:使用 http 核心库,演示以下几种应用场景的使用方式

  • 普通的 http get 请求功能演示;
  • http get 下载压缩数据的功能演示;
  • http get 下载数据保存到文件中的功能演示;
  • http post 提交表单数据功能演示;
  • http post 提交 json 数据功能演示;
  • http post 提交纯文本数据功能演示;
  • http post 提交 xml 数据功能演示;
  • http post 提交原始二进制数据功能演示;
  • http post 文件上传功能演示;

2、httpplus_app:使用 httpplus 扩展库,演示以下几种应用场景的使用方式

  • 普通的 http get 请求功能演示;
  • http get 下载压缩数据的功能演示;
  • http post 提交表单数据功能演示;
  • http post 提交 json 数据功能演示;
  • http post 提交纯文本数据功能演示;
  • http post 提交 xml 数据功能演示;
  • http post 提交原始二进制数据功能演示;
  • http post 文件上传功能演示;

3、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)

(1) netdrv_4g:4G 网卡

(2) netdrv_wifi:WIFI STA 网卡

(3) netdrv_eth_spi:通过 SPI 外挂 CH390H 芯片的以太网卡

(4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级

四、演示硬件环境

1. Air8000 开发板一块 + 可上网的 sim 卡一张 +4g 天线一根 +wifi 天线一根 + 网线一根:

  • sim 卡插入开发板的 sim 卡槽
  • 天线装到开发板上
  • 网线一端插入开发板网口,另外一端连接可以上外网的路由器网口

2. TYPE-C USB 数据线一根 + USB 转串口数据线一根,Air8000 开发板和数据线的硬件接线方式为:

  • Air8000 开发板通过 TYPE-C USB 口供电;(外部供电/USB 供电 拨动开关 拨到 USB 供电一端)
  • TYPE-C USB 数据线直接插到核心板的 TYPE-C USB 座子,另外一端连接电脑 USB 口;

五、演示软件环境

5.1 软件环境

1. 烧录工具:Luatools 下载调试工具

2. 内核固件:Air8000 V2012 版本固件(理论上,2025 年 7 月 26 日之后发布的固件都可以)

3. 脚本文件:Air8000 HTTP 脚本文件

4. LuatOS 运行所需要的 lib 文件:使用 Luatools 烧录时,勾选 添加默认 lib 选项,使用默认 lib 脚本文件。

准备好软件环境之后,接下来查看 Air8000 产品手册中“Air8000 整机开发板使用手册 -> 使用说明”,将本篇文章中演示使用的项目文件烧录到 Air8000 开发板中。

5.2 API 介绍

sys 库:https://docs.openluat.com/osapi/core/sys/

http 库:https://docs.openluat.com/osapi/core/http/

httpplu 库:https://docs.openluat.com/osapi/ext/httpplus/

六、程序结构

http/
├── main.lua
├── netdrv_device.lua
├── http_app.lua
├── httpplus_app.lua
├── logo.jpg
└── netdrv/
    ├── netdrv_4g.lua
    ├── netdrv_wifi.lua
    ├── netdrv_eth_spi.lua
    └── netdrv_multiple.lua

6.1 文件说明

  • main.lua:主程序入口文件,负责初始化系统、启动网络驱动和 HTTP 演示任务。
  • netdrv_device.lua:网络设备配置文件。
  • http_app.lua:http 核心库的演示文件,包含 13 个示例场景。
  • httpplus_app.lua:httpplus 扩展库的演示文件,包含 9 个示例场景。
  • logo.jpg:供上传、下载测试使用的素材文件。
  • netdrv/:网络驱动相关文件。
    • netdrv_4g.lua:4G 网络驱动。
    • netdrv_wifi.lua:WIFI 网络驱动。
    • netdrv_eth_spi.lua:SPI 以太网驱动。
    • netdrv_multiple.lua:多网络驱动管理。

七、核心模块详解

7.1 主程序 (main.lua)

主程序文件 main.lua 是整个项目的入口点。它负责初始化系统环境。

7.1.1 初始化流程

1. 项目和版本定义

  • 定义 PROJECTVERSION 变量。

2. 日志记录

  • 使用 log.info``("main", PROJECT, VERSION) 在日志中打印项目名和版本号。

3. 看门狗初始化(如果支持):

  • 配置并启动硬件看门狗,防止程序死循环卡死。

4. 加载功能模块

  • 加载网络驱动设备模块(netdrv_device)。
  • 加载 HTTP 核心库示例模块(http_app.lua)。
  • 加载 HTTPPLUS 扩展库示例模块(httpplus_app.lua)。

5. 启动任务调度器

  • 调用 sys.run() 启动 LuatOS 的任务调度器,开始执行各个任务。
--[[
@module  main
@summary LuatOS用户应用脚本文件入口,总体调度应用逻辑 
@version 1.0
@date    2025.07.28
@author  马梦阳
@usage
本demo演示的核心功能为:
1、分别使用http核心库和httpplus扩展库,演示以下几种应用场景的使用方式
   (1) 普通的http get请求功能演示;
   (2) http get下载压缩数据的功能演示;
   (3) http get下载数据保存到文件中的功能演示;(仅http核心库支持,httpplus扩展库不支持)
   (4) http post提交表单数据功能演示;
   (5) http post提交json数据功能演示;
   (6) http post提交纯文本数据功能演示;
   (7) http post提交xml数据功能演示;
   (8) http post提交原始二进制数据功能演示;
   (9) http post文件上传功能演示;
2、netdrv_device:配置连接外网使用的网卡,目前支持以下四种选择(四选一)
   (1) netdrv_4g:4G网卡
   (2) netdrv_wifi:WIFI STA网卡
   (3) netdrv_eth_spi:通过SPI外挂CH390H芯片的以太网卡
   (4) netdrv_multiple:支持以上三种网卡,可以配置三种网卡的优先级

更多说明参考本目录下的readme.md文件
]]

--[[
必须定义PROJECT和VERSION变量,Luatools工具会用到这两个变量,远程升级功能也会用到这两个变量
PROJECT:项目名,ascii string类型
        可以随便定义,只要不使用,就行
VERSION:项目版本号,ascii string类型
        如果使用合宙iot.openluat.com进行远程升级,必须按照"XXX.YYY.ZZZ"三段格式定义:
            X、Y、Z各表示1位数字,三个X表示的数字可以相同,也可以不同,同理三个Y和三个Z表示的数字也是可以相同,可以不同
            因为历史原因,YYY这三位数字必须存在,但是没有任何用处,可以一直写为000
        如果不使用合宙iot.openluat.com进行远程升级,根据自己项目的需求,自定义格式即可
]]
PROJECT = "HTTP"
VERSION = "001.000.000"

-- 在日志中打印项目名和项目版本号
log.info("main", PROJECT, VERSION)

-- 如果内核固件支持wdt看门狗功能,此处对看门狗进行初始化和定时喂狗处理
-- 如果脚本程序死循环卡死,就会无法及时喂狗,最终会自动重启
if wdt then
    --配置喂狗超时时间为9秒钟
    wdt.init(9000)
    --启动一个循环定时器,每隔3秒钟喂一次狗
    sys.timerLoopStart(wdt.feed, 3000)
end

-- 如果内核固件支持errDump功能,此处进行配置,【强烈建议打开此处的注释】
-- 因为此功能模块可以记录并且上传脚本在运行过程中出现的语法错误或者其他自定义的错误信息,可以初步分析一些设备运行异常的问题
-- 以下代码是最基本的用法,更复杂的用法可以详细阅读API说明文档
-- 启动errDump日志存储并且上传功能,600秒上传一次
-- if errDump then
--     errDump.config(true, 600)
-- end

-- 使用LuatOS开发的任何一个项目,都强烈建议使用远程升级FOTA功能
-- 可以使用合宙的iot.openluat.com平台进行远程升级
-- 也可以使用客户自己搭建的平台进行远程升级
-- 远程升级的详细用法,可以参考fota的demo进行使用

-- 启动一个循环定时器
-- 每隔3秒钟打印一次总内存,实时的已使用内存,历史最高的已使用内存情况
-- 方便分析内存使用是否有异常
-- sys.timerLoopStart(function()
--     log.info("mem.lua", rtos.meminfo())
--     log.info("mem.sys", rtos.meminfo("sys"))
-- end, 3000)

-- 加载网络驱动设备功能模块
require "netdrv_device"

-- 加载http应用功能模块
require "http_app"
-- 加载httpplus应用功能模块
require "httpplus_app"

-- 用户代码已结束---------------------------------------------
-- 结尾总是这一句
sys.run()
-- sys.run()之后不要加任何语句!!!!!因为添加的任何语句都不会被执行

7.2 网络驱动 (netdrv/)

网络驱动模块负责初始化和管理不同的网络连接方式,如 4G、WIFI 和以太网。

7.2.1 4G 网络驱动 (netdrv_4g.lua)

  • 监听 IP_READYIP_LOSE 消息,监控网络连接状态。
  • 设置默认网卡为 socket.LWIP_GP
--[[
@module  netdrv_4g
@summary “4G网卡”驱动模块
@version 1.0
@date    2025.07.01
@author  马梦阳
@usage
本文件为4G网卡驱动模块,核心业务逻辑为:
1、监听"IP_READY"和"IP_LOSE",在日志中进行打印;

本文件没有对外接口,直接在其他功能模块中require "netdrv_4g"就可以加载运行;
]]

local function ip_ready_func(ip, adapter)
    if adapter == socket.LWIP_GP then
        log.info("netdrv_4g.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_GP))
    end
end

local function ip_lose_func(adapter)
    if adapter == socket.LWIP_GP then
        log.warn("netdrv_4g.ip_lose_func", "IP_LOSE")
    end
end

-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察4G网络的连接状态
-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)

-- 设置默认网卡为socket.LWIP_GP
-- 在Air8000上,内核固件运行起来之后,默认网卡就是socket.LWIP_GP
-- 在单4G网卡使用场景下,下面这一行代码加不加都没有影响,为了和其他网卡驱动模块的代码风格保持一致,所以加上了
socket.dft(socket.LWIP_GP)

7.2.2 WIFI 网络驱动 (netdrv_wifi.lua)

  • 初始化 WIFI 模块,连接指定的热点(需要修改成需要连接的 WiFi 热点名称和密码,并且是 2.4G,不支持 5G WiFi)。
  • 通过将 wlan.connect(, , 1) 的第三个参数设置为 1 以开启自动重连功能。
  • 设置默认网卡为 socket.LWIP_STA
--[[
@module  netdrv_wifi
@summary “WIFI STA网卡”驱动模块
@version 1.0
@date    2025.07.01
@author  马梦阳
@usage
本文件为WIFI STA网卡驱动模块,核心业务逻辑为:
1、初始化WIFI网络;
2、连接WIFI路由器;
3、和WIFI路由器之间的连接状态发生变化时,在日志中进行打印;

本文件没有对外接口,直接在其他功能模块中require "netdrv_wifi"就可以加载运行;
]]

local function ip_ready_func(ip, adapter)
    if adapter == socket.LWIP_STA then
        log.info("netdrv_wifi.ip_ready_func", "IP_READY", json.encode(wlan.getInfo()))
    end
end

local function ip_lose_func(adapter)
    if adapter == socket.LWIP_STA then
        log.warn("netdrv_wifi.ip_lose_func", "IP_LOSE")
    end
end

-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察WIFI的连接状态
-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)

-- 设置默认网卡为socket.LWIP_STA
socket.dft(socket.LWIP_STA)

wlan.init()
-- 连接WIFI热点,连接结果会通过"IP_READY"或者"IP_LOSE"消息通知
-- Air8000仅支持2.4G的WIFI,不支持5G的WIFI
-- 此处前两个参数表示WIFI热点名称以及密码,更换为自己测试时的真实参数即可
-- 第三个参数1表示WIFI连接异常时,内核固件会自动重连
wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1)

-- WIFI联网成功(做为STATION成功连接AP,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
-- 各个功能模块可以订阅"IP_READY"消息实时处理WIFI联网成功的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功

-- WIFI断网后,内核固件会产生一个"IP_LOSE"消息
-- 各个功能模块可以订阅"IP_LOSE"消息实时处理WIFI断网的事件
-- 也可以在任何时刻调用socket.adapter(socket.LWIP_STA)来获取WIFI网络是否连接成功

7.2.3 以太网网络驱动(netdrv_eth_spi.lua)

  • 通过 SPI 接口外挂 CH390H 芯片实现以太网。
  • 通过控制 GPIO140 引脚使能芯片供电。
  • 配置 SPI1 接口参数,用于与 CH390H 芯片通信。
  • 通过 netdrv.setup 函数配置以太网卡,并开启 DHCP 动态获取 IP 地址。
  • 设置默认网卡为 socket.LWIP_ETH
--[[
@module  netdrv_eth_spi
@summary “通过SPI外挂CH390H芯片的以太网卡”驱动模块
@version 1.0
@date    2025.07.24
@author  马梦阳
@usage
本文件为“通过SPI外挂CH390H芯片的以太网卡”驱动模块,核心业务逻辑为:
1、打开CH390H芯片供电开关;
2、初始化spi1,初始化以太网卡,并且在以太网卡上开启DHCP(动态主机配置协议);
3、以太网卡的连接状态发生变化时,在日志中进行打印;

直接使用Air8000开发板硬件测试即可;

本文件没有对外接口,直接在其他功能模块中require "netdrv_eth_spi"就可以加载运行;
]]

local function ip_ready_func(ip, adapter)
    if adapter == socket.LWIP_ETH then
        log.info("netdrv_eth_spi.ip_ready_func", "IP_READY", socket.localIP(socket.LWIP_ETH))
    end
end

local function ip_lose_func(adapter)
    if adapter == socket.LWIP_ETH then
        log.warn("netdrv_eth_spi.ip_lose_func", "IP_LOSE")
    end
end

-- 此处订阅"IP_READY"和"IP_LOSE"两种消息
-- 在消息的处理函数中,仅仅打印了一些信息,便于实时观察“通过SPI外挂CH390H芯片的以太网卡”的连接状态
-- 也可以根据自己的项目需求,在消息处理函数中增加自己的业务逻辑控制,例如可以在连网状态发生改变时更新网络图标
sys.subscribe("IP_READY", ip_ready_func)
sys.subscribe("IP_LOSE", ip_lose_func)

-- 设置默认网卡为socket.LWIP_ETH
socket.dft(socket.LWIP_ETH)

-- 本demo测试使用的是Air8000开发板
-- GPIO140为CH390H以太网芯片的供电使能控制引脚
gpio.setup(140, 1, gpio.PULLUP)

-- 这个task的核心业务逻辑是:初始化SPI,初始化以太网卡,并在以太网卡上开启动态主机配置协议
local function netdrv_eth_spi_task_func()
    -- 初始化SPI1
    local result = spi.setup(
        1,--spi_id
        nil,
        0,--CPHA
        0,--CPOL
        8,--数据宽度
        25600000--,--频率
        -- spi.MSB,--高低位顺序    可选,默认高位在前
        -- spi.master,--主模式     可选,默认主
        -- spi.full--全双工       可选,默认全双工
    )
    log.info("netdrv_eth_spi", "spi open result", result)
    --返回值为0,表示打开成功
    if result ~= 0 then
        log.error("netdrv_eth_spi", "spi open error",result)
        return
    end

    -- 初始化以太网卡

    -- 以太网联网成功(成功连接路由器,并且获取到了IP地址)后,内核固件会产生一个"IP_READY"消息
    -- 各个功能模块可以订阅"IP_READY"消息实时处理以太网联网成功的事件
    -- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功

    -- 以太网断网后,内核固件会产生一个"IP_LOSE"消息
    -- 各个功能模块可以订阅"IP_LOSE"消息实时处理以太网断网的事件
    -- 也可以在任何时刻调用socket.adapter(socket.LWIP_ETH)来获取以太网是否连接成功

    -- socket.LWIP_ETH 指定网络适配器编号
    -- netdrv.CH390外挂CH390
    -- SPI ID 1, 片选 GPIO12
    netdrv.setup(socket.LWIP_ETH, netdrv.CH390, {spi=1, cs=12})

    -- 在以太网上开启动态主机配置协议
    netdrv.dhcp(socket.LWIP_ETH, true)
end

-- 创建并且启动一个task
-- task的处理函数为netdrv_eth_spi_task_func
sys.taskInit(netdrv_eth_spi_task_func)

7.2.4 多网络驱动管理 (netdrv_multiple.lua)

  • 管理多个网络驱动实例,根据配置选择合适的网络连接方式。
  • 通过 exnetif.set_priority_order 函数配置多网卡的控制参数以及优先级。
  • 通过 exnetif.notify_status 函数设置网卡状态变化通知回调函数。
--[[
@module  netdrv_multiple
@summary 多网卡(4G网卡、WIFI STA网卡、通过SPI外挂CH390H芯片的以太网卡)驱动模块
@version 1.0
@date    2025.07.24
@author  马梦阳
@usage
本文件为多网卡驱动模块,核心业务逻辑为:
1、调用exnetif.set_priority_order配置多网卡的控制参数以及优先级;

直接使用Air8000开发板硬件测试即可;

本文件没有对外接口,直接在其他功能模块中require "netdrv_multiple"就可以加载运行;
]]

local exnetif = require "exnetif"

-- 网卡状态变化通知回调函数
-- 当exnetif中检测到网卡切换或者所有网卡都断网时,会触发调用此回调函数
-- 当网卡切换切换时:
--     net_type:string类型,表示当前使用的网卡字符串
--     adapter:number类型,表示当前使用的网卡id
-- 当所有网卡断网时:
--     net_type:为nil
--     adapter:number类型,为-1
local function netdrv_multiple_notify_cbfunc(net_type,adapter)
    if type(net_type)=="string" then
        log.info("netdrv_multiple_notify_cbfunc", "use new adapter", net_type, adapter)
    elseif type(net_type)=="nil" then
        log.warn("netdrv_multiple_notify_cbfunc", "no available adapter", net_type, adapter)
    else
        log.warn("netdrv_multiple_notify_cbfunc", "unknown status", net_type, adapter)
    end
end

local function netdrv_multiple_task_func()
    --设置网卡优先级
    exnetif.set_priority_order(
        {
            -- “通过SPI外挂CH390H芯片”的以太网卡,使用Air8000开发板验证
            {
                ETHERNET = {
                    -- 供电使能GPIO
                    pwrpin = 140,
                    -- 设置的多个“已经IP READY,但是还没有ping通”网卡,循环执行ping动作的间隔(单位毫秒,可选)
                    -- 如果没有传入此参数,exnetif会使用默认值10秒
                    ping_time = 3000,

                    -- 连通性检测ip(选填参数);
                    -- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",

                    -- 网卡芯片型号(选填参数),仅spi方式外挂以太网时需要填写。
                    tp = netdrv.CH390,
                    opts = {spi=1, cs=12}
                }
            },

            -- WIFI STA网卡
            {
                WIFI = {
                    -- 要连接的WIFI路由器名称
                    ssid = "茶室-降功耗,找合宙!",
                    -- 要连接的WIFI路由器密码
                    password = "Air123456",

                    -- 连通性检测ip(选填参数);
                    -- 如果没有传入ip地址,exnetif中会默认使用httpdns能否成功获取baidu.com的ip作为是否连通的判断条件;
                    -- 如果传入,一定要传入可靠的并且可以ping通的ip地址;
                    -- ping_ip = "填入可靠的并且可以ping通的ip地址",
                }
            },

            -- 4G网卡
            {
                LWIP_GP = true
            }
        }
    )
end

-- 设置网卡状态变化通知回调函数netdrv_multiple_notify_cbfunc
exnetif.notify_status(netdrv_multiple_notify_cbfunc)

-- 如果存在udp网络应用,并且udp网络应用中,根据应用层的心跳能够判断出来udp数据通信出现了异常;
-- 可以在判断出现异常的位置,调用一次exnetif.check_network_status()接口,强制对当前正式使用的网卡进行一次连通性检测;
-- 如果存在tcp网络应用,不需要用户调用exnetif.check_network_status()接口去控制,exnetif会在tcp网络应用通信异常时自动对当前使用的网卡进行连通性检测。

-- 启动一个task,task的处理函数为netdrv_multiple_task_func
-- 在处理函数中调用exnetif.set_priority_order设置网卡优先级
-- 因为exnetif.set_priority_order要求必须在task中被调用,所以此处启动一个task
sys.taskInit(netdrv_multiple_task_func)

7.3 HTTP 库演示模块(http_app.lua

http_app.lua 是 LuatOS 中基于 http 核心库 所开发的 HTTP 应用功能模块。该模块通过调用 http.request 接口演示了 13 种 HTTP 请求场景,这 13 种演示功能如下。

7.3.1 下载回调函数

-- http下载数据回调函数
-- content_len:number类型,数据总长度
-- body_len:number类型,已经下载的数据长度
-- userdata:下载回调函数使用的用户自定义回调参数
-- 每收到一包body数据,就会调用一次http_cbfunc回调函数
local function http_cbfunc(content_len, body_len, userdata)
    log.info("http_cbfunc", content_len, body_len, userdata)
end

该函数在下载数据过程中被调用,用于实时监控下载进度。参数包括:

  • content_len:数据总长度。
  • body_len:已下载的数据长度。
  • userdata:用户自定义参数。

7.3.2 HTTP GET 请求功能

1. 普通 GET 请求

http_app_get() 函数演示了三种不同的 GET 请求场景:

1. 基本的 HTTPS GET 请求。

2. 设置超时时间和回调函数的 HTTPS GET 请求。

3. 设置短超时和回调函数的 HTTP GET 请求。

-- http下载数据回调函数
-- content_len:number类型,数据总长度
-- body_len:number类型,已经下载的数据长度
-- userdata:下载回调函数使用的用户自定义回调参数
-- 每收到一包body数据,就会调用一次http_cbfunc回调函数
local function http_cbfunc(content_len, body_len, userdata)
    log.info("http_cbfunc", content_len, body_len, userdata)
end

-- 普通的http get请求功能演示
-- 请求的body数据保存到内存变量中,在内存够用的情况下,最大支持32KB的数据存储到内存中
-- timeout可以设置超时时间
-- callback可以设置回调函数,可用于实时检测body数据的下载进度
local function http_app_get()
    -- https get请求https://www.air32.cn/网页内容
    -- 如果请求成功,请求的数据保存到body中
    local code, headers, body = http.request("GET", "https://www.air32.cn/").wait()
    log.info("http_app_get1", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        body and (body:len()>512 and body:len() or body) or "nil")

    -- https get请求https://www.luatos.com/网页内容,超时时间为10秒
    -- 请求超时时间为10秒,用户自己写代码时,不要照抄10秒,根据自己业务逻辑的需要设置合适的超时时间
    -- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get2"
    -- 如果请求成功,请求的数据保存到body中
    code, headers, body = http.request("GET", "https://www.luatos.com/", nil, nil, {timeout=10000, userdata="http_app_get2", callback=http_cbfunc}).wait()
    log.info("http_app_get2", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        body and (body:len()>512 and body:len() or body) or "nil")

    -- http get请求http://httpbin.air32.cn/get网页内容,超时时间为3秒
    -- 请求超时时间为3秒,用户自己写代码时,不要照抄3秒,根据自己业务逻辑的需要设置合适的超时时间
    -- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get3"
    -- 如果请求成功,请求的数据保存到body中
    code, headers, body = http.request("GET", "http://httpbin.air32.cn/get", nil, nil, {timeout=3000, userdata="http_app_get3", callback=http_cbfunc}).wait()
    log.info("http_app_get3", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        body and (body:len()>512 and body:len() or body) or "nil")
end

2. 下载压缩数据

http_app_get_gzip() 函数演示了如何处理压缩格式的 HTTP 响应:

  • 发送 GET 请求获取压缩的天气数据。
  • 对响应内容进行解压缩处理。
  • 解析解压后的 JSON 数据并提取信息。
-- http get下载压缩数据的功能演示
local function http_app_get_gzip()
    -- https get请求https://devapi.qweather.com/v7/weather/now?location=101010100&key=0e8c72015e2b4a1dbff1688ad54053de网页内容
    -- 如果请求成功,请求的数据保存到body中
    local code, headers, body = http.request("GET", "https://devapi.qweather.com/v7/weather/now?location=101010100&key=0e8c72015e2b4a1dbff1688ad54053de").wait()
    log.info("http_app_get_gzip", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        body and (body:len()>512 and body:len() or body) or "nil")

    -- 如果请求成功
    if code == 200 then
        -- 从body的第11个字节开始解压缩
        local uncompress_data = miniz.uncompress(body:sub(11,-1), 0)
        if not uncompress_data then
            log.error("http_app_get_gzip uncompress error")
            return
        end

        local json_data = json.decode(uncompress_data)
        if not json_data then
            log.error("http_app_get_gzip json.decode error")
            return
        end

        log.info("http_app_get_gzip json_data", json_data)
        log.info("http_app_get_gzip", "和风天气", json_data.code)
        if json_data.now then
            log.info("http_app_get_gzip", "和风天气", "天气", json_data.now.text)
            log.info("http_app_get_gzip", "和风天气", "温度", json_data.now.temp)
        end
    end
end

3. 下载数据到文件

http_app_get_file() 函数演示了如何将 HTTP 响应直接保存到文件:

  • 创建下载目录。
  • 发送 GET 请求并设置 dst 参数指定保存路径。
  • 验证下载文件的完整性。
  • 按需删除临时文件。
-- http get下载数据保存到文件中的功能演示
-- 请求的body数据保存到文件中,在文件系统够用的情况下,文件大小不限
-- timeout可以设置超时时间
-- callback可以设置回调函数,可用于实时检测文件下载进度
local function http_app_get_file()

    -- 创建/http_download目录,用来存放通过http下载的文件
    -- 重复创建目录会返回失败
    -- 在创建目录之前可以使用api判断下目录是否存在
    -- 不过只有最新版本的内核固件才支持判断目录是否存在的api
    -- 在编写本demo时还没有这个api
    -- 如果Luatools烧录软件时,没有勾选 清除FS分区,此处日志有可能输出error
    -- 如果输出error,不用理会,不会影响后续逻辑的执行
    -- 等后续的新版本内核固件支持 判断目录是否存在 的api之后,再加上api判断
    local download_dir = "/http_download/"
    local result, reason = io.mkdir(download_dir)
    if not result then
        log.error("http_app_get_file io.mkdir error", reason)
    end


    local file_path = download_dir.."get_file1.html"
    -- https get请求https://www.air32.cn/网页内容
    -- 如果请求成功,请求的数据保存到文件file_path中
    local code, headers, body_size = http.request("GET", "https://www.air32.cn/", nil, nil, {dst=file_path}).wait()
    log.info("http_app_get_file1", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        body_size)

    -- 如果下载成功
    if code==200 then
        -- 读取文件大小
        local size = io.fileSize(file_path)
        log.info("http_app_get_file1", "io.fileSize="..size)

        if size~=body_size then
            log.error("io.fileSize doesn't equal with body_size, error", size, body_size)
        end

        --文件使用完之后,如果以后不再用到,根据需要可以自行删除
        os.remove(file_path)
    end


    file_path = download_dir.."get_file2.html"
    -- https get请求https://www.luatos.com/网页内容
    -- 请求超时时间为10秒,用户自己写代码时,不要照抄10秒,根据自己业务逻辑的需要设置合适的超时时间
    -- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get_file2"
    -- 如果请求成功,请求的数据保存到文件file_path中
    code, headers, body_size = http.request("GET", "https://www.luatos.com/", nil, nil, {dst=file_path, timeout=10000, userdata="http_app_get_file2", callback=http_cbfunc}).wait()
    log.info("http_app_get_file2", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        body_size)

    -- 如果下载成功
    if code==200 then
        -- 读取文件大小
        local size = io.fileSize(file_path)
        log.info("http_app_get_file2", "io.fileSize="..size)

        if size~=body_size then
            log.error("io.fileSize doesn't equal with body_size, error", size, body_size)
        end

        --文件使用完之后,如果以后不再用到,根据需要可以自行删除
        os.remove(file_path)
    end

    file_path = download_dir.."get_file3.html"
    -- http get请求http://httpbin.air32.cn/get网页内容,超时时间为3秒
    -- 请求超时时间为3秒,用户自己写代码时,不要照抄3秒,根据自己业务逻辑的需要设置合适的超时时间
    -- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get_file3"
    -- 如果请求成功,请求的数据保存到文件file_path中
    code, headers, body_size = http.request("GET", "http://httpbin.air32.cn/get", nil, nil, {dst=file_path, timeout=3000, userdata="http_app_get_file3", callback=http_cbfunc}).wait()
    log.info("http_app_get_file3", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        body_size)

    -- 如果下载成功
    if code==200 then
        -- 读取文件大小
        local size = io.fileSize(file_path)
        log.info("http_app_get_file3", "io.fileSize="..size)

        if size~=body_size then
            log.error("io.fileSize doesn't equal with body_size, error", size, body_size)
        end

        --文件使用完之后,如果以后不再用到,根据需要可以自行删除
        os.remove(file_path)
    end
end

7.3.3 HTTP POST 请求功能

1. 提交表单数据

http_app_post_form() 函数演示了如何发送表单格式的 POST 请求:

  • 构造表单数据并进行 URL 编码。
  • 设置正确的 Content-Typeapplication/x-www-form-urlencoded)。
  • 发送请求并处理响应。
-- http post提交表单数据功能演示
local function http_app_post_form()
    local params = {
        username = "LuatOS",
        password = "123456"
    }
    local body = ""
    -- 拼接成url编码的键值对的形式
    for k, v in pairs(params) do
        body = body .. k .. "=" .. tostring(v):urlEncode() .. "&"
    end
    -- 删除最后一位的&字符,最终为string类型的username=LuatOS&password=123456
    body = body:sub(1,-2)

    -- http post提交表单数据
    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的表单数据后,还会下发同样的表单数据给设备
    -- ["Content-Type"] = "application/x-www-form-urlencoded" 表示post提交的body数据格式为url编码的键值对形式的表单数据
    -- 如果请求成功,服务器应答的数据会保存到resp_body中
    local code, headers, resp_body = http.request("POST", "http://httpbin.air32.cn/post", {["Content-Type"] = "application/x-www-form-urlencoded"}, body).wait()
    log.info("http_app_post_form", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        resp_body and (resp_body:len()>512 and resp_body:len() or resp_body) or "nil")
end

2. 提交 JSON 数据

http_app_post_json() 函数演示了如何发送 JSON 格式的 POST 请求:

  • 使用 json.encode() 将 Lua 表转换为 JSON 字符串。
  • 设置正确的 Content-Typeapplication/json)。
  • 发送请求并处理响应。
-- http post提交json数据功能演示
local function http_app_post_json()
    local params = {
        username = "LuatOS",
        password = "123456"
    }
    local body = json.encode(params)

    -- http post提交json数据
    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的json数据后,还会下发同样的json数据给设备
    -- ["Content-Type"] = "application/json" 表示post提交的body数据格式为json格式的数据
    -- 如果请求成功,服务器应答的数据会保存到resp_body中
    local code, headers, resp_body = http.request("POST", "http://httpbin.air32.cn/post", {["Content-Type"] = "application/json"}, body).wait()
    log.info("http_app_post_json", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        resp_body and (resp_body:len()>512 and resp_body:len() or resp_body) or "nil")
end

3. 提交纯文本数据

http_app_post_text() 函数演示了如何发送纯文本数据格式的 POST 请求:

  • 设置正确的 Content-Typetext/plain)。
  • 发送请求并处理响应。
-- http post提交纯文本数据功能演示
local function http_app_post_text()
    -- http post提交纯文本数据
    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的纯文本数据后,还会下发同样的纯文本数据给设备
    -- ["Content-Type"] = "text/plain" 表示post提交的body数据格式为纯文本格式的数据
    -- 如果请求成功,服务器应答的数据会保存到resp_body中
    local code, headers, resp_body = http.request("POST", "http://httpbin.air32.cn/post", {["Content-Type"] = "text/plain"}, "This is a raw text message from LuatOS device").wait()
    log.info("http_app_post_text", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        resp_body and (resp_body:len()>512 and resp_body:len() or resp_body) or "nil")
end

4. 提交 XML 数据

http_app_post_xml() 函数演示了如何发送 XML 格式的 POST 请求:

  • 使用 Lua 的长字符串语法 [=[...]=] 定义 XML 内容。
  • 设置正确的 Content-Typetext/xml)。
  • 发送请求并处理响应。
-- http post提交xml数据功能演示
local function http_app_post_xml()
    -- [=[ 和 ]=] 之间是一个多行字符串
    local body = [=[
        <?xml version="1.0" encoding="UTF-8"?>
        <user>
            <name>LuatOS</name>
            <password>123456</password>
        </user>
    ]=]

    -- http post提交xml数据
    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的xml数据后,还会下发同样的xml数据给设备
    -- ["Content-Type"] = "text/xml" 表示post提交的body数据格式为xml格式的数据
    -- 如果请求成功,服务器应答的数据会保存到resp_body中
    local code, headers, resp_body = http.request("POST", "http://httpbin.air32.cn/post", {["Content-Type"] = "text/xml"}, body).wait()
    log.info("http_app_post_xml", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        resp_body and (resp_body:len()>512 and resp_body:len() or resp_body) or "nil")
end

5. 提交原始二进制数据

http_app_post_binary() 函数演示了如何发送原始二进制格式的 POST 情况:

  • 使用 io.readFile 读取 JPG 图片文件。
  • 设置正确的 Content-Typeapplication/octet-stream)。
  • 发送请求并处理响应。
  • 上传成功后,可以通过指定网址查看上传的图片。
-- http post提交原始二进制数据功能演示
local function http_app_post_binary()
    local body = io.readFile("/luadb/logo.jpg")

    -- http post提交原始二进制数据
    -- http://upload.air32.cn/api/upload/jpg为jpg图片上传测试服务器
    -- 此处将logo.jpg的原始二进制数据做为body上传到服务器
    -- 上传成功后,电脑上浏览器打开https://www.air32.cn/upload/data/jpg/,打开对应的测试日期目录,点击具体的测试时间照片,可以查看上传的照片
    -- ["Content-Type"] = "application/octet-stream" 表示post提交的body数据格式为原始二进制格式的数据
    -- 如果请求成功,服务器应答的数据会保存到resp_body中
    local code, headers, resp_body = http.request("POST", "http://upload.air32.cn/api/upload/jpg", {["Content-Type"] = "application/octet-stream"}, body).wait()
    log.info("http_app_post_binary", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        resp_body and (resp_body:len()>512 and resp_body:len() or resp_body) or "nil")
end

7.3.4 文件上传功能

http_app_post_file() 函数演示了如何通过 HTTP 上传文件:

  • 调用辅助函数 post_multipart_form_data() 构造 multipart/form-data 格式请求。
  • 支持单文件、多文件、单文本或多文本上传。
  • 支持文本字段和文件字段混合上传。
local function post_multipart_form_data(url, params)
    local boundary = "----WebKitFormBoundary"..os.time()
    local req_headers = {
        ["Content-Type"] = "multipart/form-data; boundary="..boundary,
    }
    local body = {}

    -- 解析拼接 body
    for k,v in pairs(params) do
        if k=="texts" then
            local bodyText = ""
            for kk,vv in pairs(v) do
                print(kk,vv)
                bodyText = bodyText.."--"..boundary.."\r\nContent-Disposition: form-data; name=\""..kk.."\"\r\n\r\n"..vv.."\r\n"
            end
            table.insert(body, bodyText)
        elseif k=="files" then
            local contentType =
            {
                txt = "text/plain",             -- 文本
                jpg = "image/jpeg",             -- JPG 格式图片
                jpeg = "image/jpeg",            -- JPEG 格式图片
                png = "image/png",              -- PNG 格式图片   
                gif = "image/gif",              -- GIF 格式图片
                html = "image/html",            -- HTML
                json = "application/json",      -- JSON
            }

            for kk,vv in pairs(v) do
                if type(vv) == "table" then
                    for i=1, #vv do
                        print(kk,vv[i])
                        table.insert(body, "--"..boundary.."\r\nContent-Disposition: form-data; name=\""..kk.."\"; filename=\""..vv[i]:match("[^%/]+%w$").."\"\r\nContent-Type: "..contentType[vv[i]:match("%.(%w+)$")].."\r\n\r\n")
                        table.insert(body, io.readFile(vv[i]))
                        table.insert(body, "\r\n")
                    end
                else
                    print(kk,vv)
                    table.insert(body, "--"..boundary.."\r\nContent-Disposition: form-data; name=\""..kk.."\"; filename=\""..vv:match("[^%/]+%w$").."\"\r\nContent-Type: "..contentType[vv:match("%.(%w+)$")].."\r\n\r\n")
                    table.insert(body, io.readFile(vv))
                    table.insert(body, "\r\n")
                end
            end
        end
    end 
    table.insert(body, "--"..boundary.."--\r\n")
    body = table.concat(body)
    log.info("headers: ", "\r\n" .. json.encode(req_headers), type(body))
    log.info("body: " .. body:len() .. "\r\n" .. body)

    local code, headers, resp_body = http.request("POST", url, req_headers, body).wait()  
    log.info("post_multipart_form_data", 
        code==200 and "success" or "error", 
        code, 
        json.encode(headers or {}), 
        resp_body and (resp_body:len()>512 and resp_body:len() or resp_body) or "nil")

end

-- http post文件上传功能演示
local function http_app_post_file()
    -- 此接口post_multipart_form_data支持单文件上传、多文件上传、单文本上传、多文本上传、单/多文本+单/多文件上传
    -- http://airtest.openluat.com:2900/uploadFileToStatic 仅支持单文件上传,并且上传的文件name必须使用"uploadFile"
    -- 所以此处仅演示了单文件上传功能,并且"uploadFile"不能改成其他名字,否则会出现上传失败的应答
    -- 如果你自己的http服务支持更多类型的文本/文件混合上传,可以打开注释自行验证
    post_multipart_form_data(
        "http://airtest.openluat.com:2900/uploadFileToStatic",
        {
            -- texts = 
            -- {
            --     ["username"] = "LuatOS",
            --     ["password"] = "123456"
            -- },

            files =
            {
                ["uploadFile"] = "/luadb/logo.jpg",
                -- ["logo1.jpg"] = "/luadb/logo.jpg",
            }
        }
    )
end

7.3.5 任务管理机制

1. 主任务函数

该函数是模块的核心控制逻辑,实现了:

  • 网络连接等待机制:通过循环检测 socket.adapter(socket.dft())sys.waitUntil("IP_READY", 1000) 等待网络就绪。
  • 功能调用序列:按顺序调用所有 HTTP 功能演示函数。
  • 循环执行策略:每次功能演示完成后等待 60 秒,然后再次开始循环。
-- http app task 的任务处理函数
local function http_app_task_func()
    while true do
        -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
        while not socket.adapter(socket.dft()) do
            log.warn("http_app_task_func", "wait IP_READY", socket.dft())
            -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
            -- 或者等待1秒超时退出阻塞等待状态;
            -- 注意:此处的1000毫秒超时不要修改的更长;
            -- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
            -- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
            sys.waitUntil("IP_READY", 1000)
        end

        -- 检测到了IP_READY消息
        log.info("http_app_task_func", "recv IP_READY", socket.dft())

        -- 普通的http get请求功能演示
        http_app_get()
        -- http get下载压缩数据的功能演示
        http_app_get_gzip()
        -- http get下载数据保存到文件中的功能演示
        http_app_get_file()
        -- http post提交表单数据功能演示
        http_app_post_form()
        -- http post提交json数据功能演示
        http_app_post_json()
        -- http post提交纯文本数据功能演示
        http_app_post_text()
        -- http post提交xml数据功能演示
        http_app_post_xml()
        -- http post提交原始二进制数据功能演示
        http_app_post_binary()
        -- http post文件上传功能演示
        http_app_post_file()

        -- 60秒之后,循环测试
        sys.wait(60000)
    end
end

2. 任务启动

通过 sys.taskInit 函数创建并启动一个新的任务来运行 httpplus_app_task_func 函数,使整个模块的功能在后台持续运行。

--创建并且启动一个task
--运行这个task的处理函数http_app_task_func
sys.taskInit(http_app_task_func)

7.4 HTTPPLUS 库演示模块(httpplus_app.lua)

httpplus_app.lua 是 LuatOS 中基于 httpplus 扩展库所开发的 HTTP 应用功能模块。该模块通过调用 httpplus.request 接口演示了 9 种 HTTP 请求场景,这 9 种演示功能如下。

7.4.1 HTTP GET 请求功能

1. 普通 GET 请求

httpplus_app_get() 函数演示了两种不同的 GET 请求场景:

1. 基本的 HTTPS GET 请求。

2. 设置超时时间的 HTTP GET 请求。

-- 普通的http get请求功能演示
-- 请求的body数据保存到内存变量中,在内存够用的情况下,长度不限
-- timeout可以设置超时时间
local function httpplus_app_get()
    local body
    -- https get请求https://httpbin.air32.cn/get网页内容
    -- 如果请求成功,请求的数据保存到response.body中
    local code, response = httpplus.request({url="https://httpbin.air32.cn/get"})
    log.info("httpplus_app_get1", code==200 and "success" or "error", code)
    if code==200 then
        log.info("httpplus_app_get1 headers", json.encode(response.headers or {}))
        body = response.body:query()
        log.info("httpplus_app_get1 body", body and (body:len()>512 and body:len() or body) or "nil")
    end

    -- http get请求http://httpbin.air32.cn/get网页内容,超时时间为3秒
    -- 请求超时时间为3秒,用户自己写代码时,不要照抄3秒,根据自己业务逻辑的需要设置合适的超时时间
    -- 如果请求成功,请求的数据保存到body中
    code, response = httpplus.request({url="http://httpbin.air32.cn/get", timeout=3})
    log.info("httpplus_app_get2", code==200 and "success" or "error", code)
    if code==200 then
        log.info("httpplus_app_get2 headers", json.encode(response.headers or {}))
        body = response.body:query()
        log.info("httpplus_app_get2 body", body and (body:len()>512 and body:len() or body) or "nil")
    end
end

2. 下载压缩数据

httpplus_app_get_gzip() 函数演示了如何处理压缩格式的 HTTP 响应:

  • 发送 GET 请求获取压缩的天气数据。
  • 对响应内容进行解压缩处理。
  • 解析解压后的 JSON 数据并提取信息。
-- http get下载压缩数据的功能演示
local function httpplus_app_get_gzip()
    local body
    -- https get请求https://devapi.qweather.com/v7/weather/now?location=101010100&key=0e8c72015e2b4a1dbff1688ad54053de网页内容,超时时间为3秒
    -- 如果请求成功,请求的数据保存到response.body中
    local code, response = httpplus.request({url="https://devapi.qweather.com/v7/weather/now?location=101010100&key=0e8c72015e2b4a1dbff1688ad54053de"})
    log.info("httpplus_app_get_gzip", code==200 and "success" or "error", code)
    if code==200 then
        log.info("httpplus_app_get_gzip headers", json.encode(response.headers or {}))
        body = response.body:query()
        log.info("httpplus_app_get_gzip body", body and (body:len()>512 and body:len() or body) or "nil")
    end

    -- 如果请求成功
    if code == 200 then
        -- 从body的第11个字节开始解压缩
        local uncompress_data = miniz.uncompress(body:sub(11,-1), 0)
        if not uncompress_data then
            log.error("httpplus_app_get_gzip uncompress error")
            return
        end

        local json_data = json.decode(uncompress_data)
        if not json_data then
            log.error("httpplus_app_get_gzip json.decode error")
            return
        end

        log.info("httpplus_app_get_gzip json_data", json_data)
        log.info("httpplus_app_get_gzip", "和风天气", json_data.code)
        if json_data.now then
            log.info("httpplus_app_get_gzip", "和风天气", "天气", json_data.now.text)
            log.info("httpplus_app_get_gzip", "和风天气", "温度", json_data.now.temp)
        end
    end
end

7.4.2 HTTP POST 请求功能

1. 提交表单数据

httpplus_app_post_form() 函数演示了如何发送表单格式的 POST 请求:

  • 构造表单数据并进行 URL 编码。
  • 存在 forms 参数并且不存在 files 参数,系统自动强制以 application/x-www-form-urlencoded 形式上传数据。
  • 发送请求并处理响应。
-- http post提交表单数据功能演示
local function httpplus_app_post_form()
    -- http post提交表单数据
    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的表单数据后,还会下发同样的表单数据给设备
    -- 如果请求成功,服务器应答的数据会保存到response.body中
    local code, response = httpplus.request(
    {
        url = "http://httpbin.air32.cn/post",
        forms = {username="LuatOS", password="123456"}
    })
    log.info("httpplus_app_post_form", code==200 and "success" or "error", code)
    if code==200 then
        log.info("httpplus_app_post_form headers", json.encode(response.headers or {}))
        local body = response.body:query()
        log.info("httpplus_app_post_form body", body and (body:len()>512 and body:len() or body) or "nil")
    end
end

2. 提交 JSON 数据

httpplus_app_post_json() 函数演示了如何发送 JSON 格式的 POST 请求:

  • 使用 json.encode() 将 Lua 表转换为 JSON 字符串。
  • 设置正确的 Content-Typeapplication/json)。
  • 发送请求并处理响应。
-- http post提交json数据功能演示
local function httpplus_app_post_json()
    local params = {
        username = "LuatOS",
        password = "123456"
    }

    -- http post提交json数据
    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的json数据后,还会下发同样的json数据给设备
    -- ["Content-Type"] = "application/json" 表示post提交的body数据格式为json格式的数据
    -- 如果请求成功,服务器应答的数据会保存到response.body中
    local code, response = httpplus.request(
    {
        method = "POST",
        url = "http://httpbin.air32.cn/post",
        headers = {["Content-Type"] = "application/json"},
        body = json.encode(params)
    })
    log.info("httpplus_app_post_json", code==200 and "success" or "error", code)
    if code==200 then
        log.info("httpplus_app_post_json headers", json.encode(response.headers or {}))
        local body = response.body:query()
        log.info("httpplus_app_post_json body", body and (body:len()>512 and body:len() or body) or "nil")
    end
end

3. 提交纯文本数据

httpplus_app_post_text() 函数演示了如何发送纯文本数据格式的 POST 请求:

  • 设置正确的 Content-Typetext/plain)。
  • 发送请求并处理响应。
-- http post提交纯文本数据功能演示
local function httpplus_app_post_text()
    -- http post提交纯文本数据
    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的纯文本数据后,还会下发同样的纯文本数据给设备
    -- ["Content-Type"] = "text/plain" 表示post提交的body数据格式为纯文本格式的数据
    -- 如果请求成功,服务器应答的数据会保存到response.body中
    local code, response = httpplus.request(
    {
        method = "POST",
        url = "http://httpbin.air32.cn/post",
        headers = {["Content-Type"] = "text/plain"},
        body = "This is a raw text message from LuatOS device"
    })
    log.info("httpplus_app_post_text", code==200 and "success" or "error", code)
    if code==200 then
        log.info("httpplus_app_post_text headers", json.encode(response.headers or {}))
        local body = response.body:query()
        log.info("httpplus_app_post_text body", body and (body:len()>512 and body:len() or body) or "nil")
    end
end

4. 提交 XML 数据

httpplus_app_post_xml() 函数演示了如何发送 XML 格式的 POST 请求:

  • 使用 Lua 的长字符串语法 [=[...]=] 定义 XML 内容。
  • 设置正确的 Content-Typetext/xml)。
  • 发送请求并处理响应。
-- http post提交xml数据功能演示
local function httpplus_app_post_xml()
    -- [=[ 和 ]=] 之间是一个多行字符串
    local body = [=[
        <?xml version="1.0" encoding="UTF-8"?>
        <user>
            <name>LuatOS</name>
            <password>123456</password>
        </user>
    ]=]

    -- http post提交xml数据
    -- http://httpbin.air32.cn/post为回环测试服务器,服务器收到post提交的xml数据后,还会下发同样的xml数据给设备
    -- ["Content-Type"] = "text/xml" 表示post提交的body数据格式为xml格式的数据
    -- 如果请求成功,服务器应答的数据会保存到response.body中
    local code, response = httpplus.request(
    {
        method = "POST",
        url = "http://httpbin.air32.cn/post",
        headers = {["Content-Type"] = "text/xml"},
        body = body
    })
    log.info("httpplus_app_post_xml", code==200 and "success" or "error", code)
    if code==200 then
        log.info("httpplus_app_post_xml headers", json.encode(response.headers or {}))
        body = response.body:query()
        log.info("httpplus_app_post_xml body", body and (body:len()>512 and body:len() or body) or "nil")
    end
end

5. 提交原始二进制数据

http_app_post_binary() 函数演示了如何发送原始二进制格式的 POST 情况:

  • 使用 io.readFile 读取 JPG 图片文件。
  • 设置正确的 Content-Typeapplication/octet-stream)。
  • 发送请求并处理响应。
  • 上传成功后,可以通过指定网址查看上传的图片。
-- http post提交原始二进制数据功能演示
local function httpplus_app_post_binary()
    local body = io.readFile("/luadb/logo.jpg")

    -- http post提交原始二进制数据
    -- http://upload.air32.cn/api/upload/jpg为jpg图片上传测试服务器
    -- 此处将logo.jpg的原始二进制数据做为body上传到服务器
    -- 上传成功后,电脑上浏览器打开https://www.air32.cn/upload/data/jpg/,打开对应的测试日期目录,点击具体的测试时间照片,可以查看上传的照片
    -- ["Content-Type"] = "application/octet-stream" 表示post提交的body数据格式为原始二进制格式的数据
    -- 如果请求成功,服务器应答的数据会保存到response.body中
    local code, response = httpplus.request(
    {
        method = "POST",
        url = "http://upload.air32.cn/api/upload/jpg",
        headers = {["Content-Type"] = "application/octet-stream"},
        body = body
    })
    log.info("httpplus_app_post_binary", code==200 and "success" or "error", code)
    if code==200 then
        log.info("httpplus_app_post_binary headers", json.encode(response.headers or {}))
        body = response.body:query()
        log.info("httpplus_app_post_binary body", body and (body:len()>512 and body:len() or body) or "nil")
    end
end

7.4.3 文件上传功能

http_app_post_file() 函数演示了如何通过 HTTP 上传文件:

  • 使用 files 参数上传文件。
  • 系统会自动将请求设置为 POST 方法,并以 multipart/form-data 格式发送数据。
  • 支持单文件、多文件、单文本或多文本上传。
  • 支持文本字段和文件字段混合上传。
-- http post文件上传功能演示
local function httpplus_app_post_file()
    -- hhtplus.request接口支持单文件上传、多文件上传、单文本上传、多文本上传、单/多文本+单/多文件上传
    -- http://airtest.openluat.com:2900/uploadFileToStatic 仅支持单文件上传,并且上传的文件name必须使用"uploadFile"
    -- 所以此处仅演示了单文件上传功能,并且"uploadFile"不能改成其他名字,否则会出现上传失败的应答
    -- 如果你自己的http服务支持更多类型的文本/文件混合上传,可以打开注释自行验证
    local code, response = httpplus.request(
    {
        url = "http://airtest.openluat.com:2900/uploadFileToStatic",
        files =
        {
            ["uploadFile"] = "/luadb/logo.jpg",
            -- ["logo1.jpg"] = "/luadb/logo.jpg",
        },
        -- forms =
        -- {
        --     ["username"] = "LuatOS",
        --     ["password"] = "123456",
        -- },
    })
    log.info("httpplus_app_post_file", code==200 and "success" or "error", code)
    if code==200 then
        log.info("httpplus_app_post_file headers", json.encode(response.headers or {}))
        local body = response.body:query()
        log.info("httpplus_app_post_file body", body and (body:len()>512 and body:len() or body) or "nil")
    end
end

7.4.4 任务管理机制

1. 主任务函数

该函数是模块的核心控制逻辑,实现了:

  • 网络连接等待机制:通过循环检测 socket.adapter(socket.dft())sys.waitUntil("IP_READY", 1000) 等待网络就绪。
  • 功能调用序列:按顺序调用所有 HTTP 功能演示函数。
  • 循环执行策略:每次功能演示完成后等待 60 秒,然后再次开始循环。
-- http app task 的任务处理函数
local function httpplus_app_task_func() 
    while true do
        -- 如果当前时间点设置的默认网卡还没有连接成功,一直在这里循环等待
        while not socket.adapter(socket.dft()) do
            log.warn("httpplus_app_task_func", "wait IP_READY", socket.dft())
            -- 在此处阻塞等待默认网卡连接成功的消息"IP_READY"
            -- 或者等待1秒超时退出阻塞等待状态;
            -- 注意:此处的1000毫秒超时不要修改的更长;
            -- 因为当使用exnetif.set_priority_order配置多个网卡连接外网的优先级时,会隐式的修改默认使用的网卡
            -- 当exnetif.set_priority_order的调用时序和此处的socket.adapter(socket.dft())判断时序有可能不匹配
            -- 此处的1秒,能够保证,即使时序不匹配,也能1秒钟退出阻塞状态,再去判断socket.adapter(socket.dft())
            sys.waitUntil("IP_READY", 1000)
        end

        -- 检测到了IP_READY消息
        log.info("httpplus_app_task_func", "recv IP_READY", socket.dft())

        -- 普通的http get请求功能演示
        httpplus_app_get()
        -- http get下载压缩数据的功能演示
        httpplus_app_get_gzip()
        -- http post提交表单数据功能演示
        httpplus_app_post_form()
        -- -- http post提交json数据功能演示
        httpplus_app_post_json()
        -- http post提交纯文本数据功能演示
        httpplus_app_post_text()
        -- http post提交xml数据功能演示
        httpplus_app_post_xml()
        -- http post提交原始二进制数据功能演示
        httpplus_app_post_binary()
        -- http post文件上传功能演示
        httpplus_app_post_file()

        -- 60秒之后,循环测试
        sys.wait(60000)
    end
end

2. 任务启动

通过 sys.taskInit 函数创建并启动一个新的任务来运行 httpplus_app_task_func 函数,使整个模块的功能在后台持续运行。

--创建并且启动一个task
--运行这个task的处理函数httpplus_app_task_func
sys.taskInit(httpplus_app_task_func)

八、演示功能

8.1 不同网卡切换

Air8000 模组支持单 4g 网卡,单 wifi 网卡,单 spi 以太网卡,多网卡。

切换网卡为 4G 网卡:

netdrv_device.lua 模块里只打开 netdrv_4g 模块。netdrv_4g.lua 模块中的代码不需要修改。

Luatools 工具日志打印

如下图所示,如出现类似 I/user.netdrv_4g.ip_ready_func IP_READY 10.61.65.52 255.255.255.255 0.0.0.0 nil 的日志,则表示 4g 网卡连接成功。

切换网卡为 WiFi 网卡:

注意:如果需要单 WIFI STA 网卡,打开 require "netdrv_wifi",其余注释掉;同时 netdrv_wifi.lua 中的 wlan.connect("茶室-降功耗,找合宙!", "Air123456", 1),前两个参数,修改为自己测试时 wifi 热点的名称和密码;注意:仅支持 2.4G 的 wifi,不支持 5G 的 wifi

netdrv_device.lua 模块里只打开 netdrv_wifi 模块。netdrv_wifi 模块中的代码不需要修改。

Luatools 工具日志打印:

如出现类似 I/user.netdrv_wifi.ip_ready_func IP_READY {"gw":"192.168.219.194","rssi":-23,"bssid":"722D10C7C9CF"} 的日志,则表示 WIFI STA 网卡联网成功。

切换网卡为以太网卡:

注意:Air8000 的以太网卡是通过 SPI 外挂 CH390H 芯片实现的。

netdrv_device.lua 模块里只打开 netdrv_eth_spi 模块。如果是使用合宙官方的开发板,netdrv_eth_spi 模块中的代码不需要修改。

Luatools 工具日志打印:

如出现类似 I/user.netdrv_eth_spi.ip_ready_func IP_READY 192.168.71.65 255.255.255.0 192.168.71.1 nil 的日志,则表示以太网卡联网成功。

多网卡自动切换:

如果需要多网卡,打开 require "netdrv_multiple",其余注释掉;同时 netdrv_multiple.lua 中的 ssid = "茶室-降功耗,找合宙!", password = "Air123456", 修改为自己测试时 wifi 热点的名称和密码;注意:仅支持 2.4G 的 wifi,不支持 5G 的 wifi。 可根据自己的需求调整网卡的优先级,以下示例设置为以太网卡是最高优先级。

首先在 netdrv_device.lua 文件中只打开 netdrv_multiple 模块。

默认以太网卡进行连接

拔掉网线后,网络切换为 wifi 网卡

关闭设备连接的 wifi 热点,切换为 4g 网卡

8.2 HTTP 请求结果

前面介绍了 HTTP 核心库演示模块中演示了 13 种场景的 HTTP 请求,HTTPPLUS 扩展库演示模块中演示了 9 种 HTTP 请求。因此我们烧录程序成功后,在日志中搜索 success 200 ,程序默认每隔 1 分钟测试一轮,如果每轮出现 22 次 success 200,如以下日志所示,就表示成功,如果不够 22 次,则说明部分域名没有请求成功,此时可以通过详细日志所表示的含义,结合代码自行分析。

[2025-08-06 15:34:56.201][000000007.113] I/user.http_app_get1 success 200 {"Transfer-
[2025-08-06 15:34:56.354][000000007.271] I/user.httpplus_app_get1 success 200
[2025-08-06 15:34:56.622][000000007.537] I/user.httpplus_app_get2 success 200
[2025-08-06 15:34:57.896][000000008.796] I/user.httpplus_app_get_gzip success 200
[2025-08-06 15:34:58.287][000000009.070] I/user.http_app_get2 success 200 {"Vary":"Ac
[2025-08-06 15:34:58.369][000000009.112] I/user.httpplus_app_post_form success 200
[2025-08-06 15:34:58.592][000000009.248] I/user.http_app_get3 success 200 {"Connectio
[2025-08-06 15:34:58.765][000000009.412] I/user.httpplus_app_post_json success 200
[2025-08-06 15:34:59.043][000000009.951] I/user.http_app_get_gzip success 200 {"Conte
[2025-08-06 15:34:59.291][000000010.065] I/user.httpplus_app_post_text success 200
[2025-08-06 15:34:59.820][000000010.736] I/user.httpplus_app_post_xml success 200
[2025-08-06 15:34:59.903][000000010.744] I/user.http_app_get_file1 success 200 {"Tran
[2025-08-06 15:35:00.706][000000011.503] I/user.httpplus_app_post_binary success 200
[2025-08-06 15:35:01.008][000000011.862] I/user.http_app_get_file2 success 200 {"Vary
[2025-08-06 15:35:01.094][000000011.917] I/user.httpplus_app_post_file success 200
[2025-08-06 15:35:01.215][000000012.079] I/user.http_app_get_file3 success 200 {"Conn
[2025-08-06 15:35:01.356][000000012.270] I/user.http_app_post_form success 200 {"Conn
[2025-08-06 15:35:01.569][000000012.479] I/user.http_app_post_json success 200 {"Conn
[2025-08-06 15:35:01.769][000000012.681] I/user.http_app_post_text success 200 {"Conn
[2025-08-06 15:35:01.949][000000012.858] I/user.http_app_post_xml success 200 {"Conne
[2025-08-06 15:35:02.236][000000013.145] I/user.http_app_post_binary success 200 {"Da
[2025-08-06 15:35:02.437][000000013.348] I/user.post_multipart_form_data success 200

九、总结

至此,我们演示了使用不同网卡在 22 种场景下进行 HTTP 请求的全过程,相信聪明的你已经完全领悟 HTTP 请求的逻辑了,快来实际操作一下吧!