--[[ @module http_app @summary http应用功能模块 @version 1.0 @date 2025.08.01 @author 马梦阳 @usage 本文件为http应用功能模块,核心业务逻辑为:基于不同的应用场景,演示http核心库的使用方式; http核心库和httpplus扩展库的区别如下: | 区别项 | http核心库 | httpplus扩展库 | | --------------------------------- | ------------------------------------------------------------------------- | ---------------------------- | | 文件上传 | 文件最大64KB | 只要内存够用,文件大小不限 | | 文件下载 | 支持,只要文件系统空间够用,文件大小不限 | 不支持 | | http header的key: value的限制 | 所有header的value总长度不能超过4KB,单个header的value长度不能超过1KB | 只要内存够用,header长度不限 | | 鉴权URL自动识别 | 不支持 | 支持 | | 接收到的body数据存储支持zbuff | 不支持 | 支持,可以直接传输给uart等库 | | 接收到的body数据存储到内存中 | 最大支持32KB | 只要内存够用,大小不限 | | chunk编码 | 支持 | 不支持 | 本文件没有对外接口,直接在main.lua中require "http_app"就可以加载运行; ]] -- 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://httpbin.luatos.com/get网页内容 -- 如果请求成功,请求的数据保存到body中 local code, headers, body = http.request("GET", "https://httpbin.luatos.com/get").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.luatos.com/get网页内容,超时时间为3秒 -- 请求超时时间为3秒,用户自己写代码时,不要照抄3秒,根据自己业务逻辑的需要设置合适的超时时间 -- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get3" -- 如果请求成功,请求的数据保存到body中 code, headers, body = http.request("GET", "http://httpbin.luatos.com/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 -- 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 -- http get下载数据保存到文件中的功能演示 -- 请求的body数据保存到文件中,在文件系统够用的情况下,文件大小不限 -- timeout可以设置超时时间 -- callback可以设置回调函数,可用于实时检测文件下载进度 local function http_app_get_file() -- 创建/http_download目录,用来存放通过http下载的文件 -- 重复创建目录会返回失败 -- 在创建目录之前可以使用io.dexist判断下目录是否存在 -- io.dexist接口仅新版本支持(Air780系列需要使用V2014及以上固件) -- 若有报错提示,请检查是否是因为使用了旧版本的内核固件 local download_dir = "/http_download/" if not io.dexist(download_dir) then local result, reason = io.mkdir(download_dir) if not result then log.error("http_app_get_file io.mkdir error", reason) end end local file_path = download_dir.."get_file1.html" -- https get请求https://httpbin.luatos.com/get网页内容 -- 如果请求成功,请求的数据保存到文件file_path中 local code, headers, body_size = http.request("GET", "https://httpbin.luatos.com/get", 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.luatos.com/get网页内容,超时时间为3秒 -- 请求超时时间为3秒,用户自己写代码时,不要照抄3秒,根据自己业务逻辑的需要设置合适的超时时间 -- 回调函数为http_cbfunc,回调函数使用的第三个回调参数为"http_app_get_file3" -- 如果请求成功,请求的数据保存到文件file_path中 code, headers, body_size = http.request("GET", "http://httpbin.luatos.com/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 -- 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.luatos.com/post为回环测试服务器,服务器收到post提交的表单数据后,还会下发同样的表单数据给设备 -- ["Content-Type"] = "application/x-www-form-urlencoded" 表示post提交的body数据格式为url编码的键值对形式的表单数据 -- 如果请求成功,服务器应答的数据会保存到resp_body中 local code, headers, resp_body = http.request("POST", "http://httpbin.luatos.com/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 -- 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.luatos.com/post为回环测试服务器,服务器收到post提交的json数据后,还会下发同样的json数据给设备 -- ["Content-Type"] = "application/json" 表示post提交的body数据格式为json格式的数据 -- 如果请求成功,服务器应答的数据会保存到resp_body中 local code, headers, resp_body = http.request("POST", "http://httpbin.luatos.com/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 -- http post提交纯文本数据功能演示 local function http_app_post_text() -- http post提交纯文本数据 -- http://httpbin.luatos.com/post为回环测试服务器,服务器收到post提交的纯文本数据后,还会下发同样的纯文本数据给设备 -- ["Content-Type"] = "text/plain" 表示post提交的body数据格式为纯文本格式的数据 -- 如果请求成功,服务器应答的数据会保存到resp_body中 local code, headers, resp_body = http.request("POST", "http://httpbin.luatos.com/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 -- http post提交xml数据功能演示 local function http_app_post_xml() -- [=[ 和 ]=] 之间是一个多行字符串 local body = [=[ LuatOS 123456 ]=] -- http post提交xml数据 -- http://httpbin.luatos.com/post为回环测试服务器,服务器收到post提交的xml数据后,还会下发同样的xml数据给设备 -- ["Content-Type"] = "text/xml" 表示post提交的body数据格式为xml格式的数据 -- 如果请求成功,服务器应答的数据会保存到resp_body中 local code, headers, resp_body = http.request("POST", "http://httpbin.luatos.com/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 -- http post提交原始二进制数据功能演示 local function http_app_post_binary() local body = io.readFile("/luadb/logo.jpg") -- http post提交原始二进制数据 -- http://uploadtest.luatos.com/api/upload/jpg为jpg图片上传测试服务器 -- 此处将logo.jpg的原始二进制数据做为body上传到服务器 -- 上传成功后,电脑上浏览器打开https://uploadtest.luatos.com/upload/jpg/,打开对应的测试日期目录,点击具体的测试时间照片,可以查看上传的照片 -- ["Content-Type"] = "application/octet-stream" 表示post提交的body数据格式为原始二进制格式的数据 -- 如果请求成功,服务器应答的数据会保存到resp_body中 -- 因为Air32.com平台已经不开放使用了,所以该DEMO仅作上传照片至服务器的演示作用,使用时请将上传URL修改为您自己的服务器地址 -- 或者通过excloud扩展库上传到合宙IOT平台uploadtest.luatos.com local code, headers, resp_body = http.request("POST", "http://uploadtest.luatos.com/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 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支持单文件上传、多文件上传、单文本上传、多文本上传、单/多文本+单/多文件上传 -- https://airtest.luatos.com/iot/luat_test_file/add 只支持单文件上传或者单文件+单文本上传 -- 要求上传的文件name必须使用"f",上传的文本name必须使用"params" -- 所以此处仅演示了单文件+单文本上传功能,并且"f"和"params"不能改成其他名字,否则会出现上传失败的应答 -- 测试接口的响应说明: -- 成功:HTTP 200 OK:{"code":0,"value":"上传成功"}; -- 失败:HTTP 状态码非 200 OK 或是 200 OK 但 code 不为 0 -- 如何在网页端查看上传的文件: -- 在浏览器中打开https://iot.luatos.com/#/p8000/netlab_file_server,即可查看上传的文件; -- 如果你自己的http服务支持更多类型的文本/文件混合上传,可以打开注释自行验证 post_multipart_form_data( "https://airtest.luatos.com/iot/luat_test_file/add", { texts = { ["params"] = json.encode({ username = "LuatOS", password = "123456" }), -- ["username"] = "LuatOS", -- ["password"] = "123456", }, files = { ["f"] = "/luadb/logo.jpg", -- ["logo1.jpg"] = "/luadb/logo.jpg", } } ) end -- 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 --创建并且启动一个task --运行这个task的处理函数http_app_task_func sys.taskInit(http_app_task_func)