SimFAS HTTP 模块使用说明
HTTP编程文档,每一段示例都在真实服务器上跑过。
1. 三分钟上手
-- 最简单的 GET
res = http.get("http://192.168.2.100/api/status");
print("设备返回:", res);
-- 提交一个表单
res = http.post("http://192.168.2.100/ctrl", "id=8&act=on");
-- 提交 JSON(REST 接口常用)
res = http.postJSON("http://192.168.2.100/api/power", `{"power":"on"}`);三件事先记住,能少踩 90% 的坑:
- 返回的是"响应体",不含响应头;请求失败或服务器不是
200时返回 nil(空)。 - JSON 用反引号
`包起来,里面的双引号不用转义(见第 5 节)。 - 引擎不支持函数套函数:
print("len:", str.len(res))是不行的,
必须先赋值再用:
n = str.len(res);
print("len:", n);2. 函数速查
| 函数 | 方法 | 请求体 | Content-Type |
|---|---|---|---|
http.get(url) | GET | 无 | — |
http.post(url, data) | POST | 有 | application/x-www-form-urlencoded; charset=utf-8 |
http.postJSON(url, json) | POST | 有 | application/json; charset=utf-8 |
http.put(url, json) | PUT | 有 | application/json; charset=utf-8 |
http.patch(url, json) | PATCH | 有 | application/json; charset=utf-8 |
http.delete(url) | DELETE | 无(Content-Length: 0) | — |
http.set_header(key, value) | — | — | 追加一条自定义请求头 |
http.clear_headers() | — | — | 清空自定义请求头 |
- 函数名大小写不敏感:
http.postJSON和HTTP.POSTJSON等价。 http.clear_header()(不带 s)是http.clear_headers()的别名。- 只支持
http://,不支持https://。要走 HTTPS 请在前面放一层反向代理。
3. 各函数详解
3.1 http.get(url)
-- 基本用法
res = http.get("http://192.168.2.100/api/status");
-- URL 可以是拼出来的变量
ip = "192.168.2.100";
url = str.cat("http://", ip, "/api/status");
res = http.get(url);
-- 带查询参数
res = http.get("http://192.168.2.100/api/dev?id=8&act=query");
-- 不写路径也可以,等价于请求 "/"
res = http.get("http://192.168.2.100:8080");- 端口默认 80,要用别的端口就写
http://ip:端口/路径。 - 查询参数里如果有中文、空格、
&、=这些字符,必须先编码,见第 8 节。
3.2 http.post(url, data) —— 表单 POST
发的是传统表单格式,Content-Type 是 application/x-www-form-urlencoded。
老式的设备 Web 后台、PHP 页面基本都用这个。
-- 直接写
res = http.post("http://192.168.2.100/ctrl", "id=8&act=on&level=75");
-- 参数拼出来
id = 8;
act = "on";
body = str.cat("id=", id, "&act=", act);
res = http.post("http://192.168.2.100/ctrl", body);
-- 空 body 也允许(有些接口就是靠 URL 区分动作)
res = http.post("http://192.168.2.100/reboot", "");data 传整数也可以,会按十进制字符串发出去。3.3 http.postJSON(url, json) —— REST 接口最常用
res = http.postJSON("http://192.168.2.100/api/power", `{"power":"on"}`);请求头长这样(自定义头会追加在 Content-Length 后面):
POST /api/power HTTP/1.1
HOST: 192.168.2.100:80
Accept: */*
User-Agent: Mozilla/5.0 ...
Content-Type:application/json; charset=utf-8
Content-Length: 15
{"power":"on"}3.4 http.put(url, json) / http.patch(url, json)
和 postJSON 完全一样,只是方法名不同。REST 的约定:
PUT—— 整体替换一个资源PATCH—— 只改其中几个字段
res = http.put ("http://192.168.2.100/api/device/1", `{"name":"主会议室","power":"on","volume":30}`);
res = http.patch("http://192.168.2.100/api/device/1", `{"volume":30}`);3.5 http.delete(url)
没有请求体。
res = http.delete("http://192.168.2.100/api/device/1");3.6 http.set_header(key, value) / http.clear_headers()
给后面的请求加自定义头,最典型的就是鉴权:
http.set_header("Authorization", "Bearer AbC123XyZ");
http.set_header("X-Device-Id", "meeting-room-3");
res = http.get("http://192.168.2.100/api/status"); -- 两条头都会带上
http.clear_headers(); -- 用完清掉必须知道的四条规则:
- 设了就一直有效,直到调用
http.clear_headers()或脚本结束。
不清掉的话,后面每一个http.*请求都会带上它。 - 对所有方法都生效,
get/post/postJSON/put/patch/delete一视同仁。 - 最多 8 条,超出的会被静默丢弃;单条的 key 和 value 各最长 255 字节。
- 是"追加",不是"替换"。设
Content-Type这种内置就有的头,请求里会出现两条,
多数服务器取后一条,但不保证 —— 尽量别去覆盖Host/Content-Type/Content-Length。
自定义头是每个脚本线程各自一份的:A 脚本设的头不会影响同时在跑的 B 脚本。
反过来说,同一个脚本里设了就得自己清。
4. 返回值与错误处理
成功:返回响应体字符串(不含响应头)。
失败:返回 nil,此时目标变量是空的。
以下情况都算失败,返回 nil:
| 情况 | 说明 |
|---|---|
| 连不上(地址错 / 端口没开 / 网线断) | 3 秒内返回 |
| 服务器不回包 | 5 秒收包超时后返回 |
| 响应状态码不是 200 | 404 / 401 / 500 都返回 nil |
响应体为空(Content-Length: 0) | 返回 nil |
URL 不是 http:// 开头 | 立刻返回 |
| DNS 解析不了域名 | 立刻返回 |
怎么判断成功
用 str.len(),不要用 str.cmp(res,"")。(对 nil 变量 str.cmp 返回 0,会误判)
res = http.get("http://192.168.2.100/api/status");
n = str.len(res);
if (n < 1)
print("请求失败");
else
print("拿到:", res);
end状态码拿不到怎么办
引擎只把 200 的响应体给你,拿不到状态码。
所以业务上的成功/失败,要么靠"有没有返回内容",要么靠服务器自己在 JSON 里带的
业务码(几乎所有 REST 接口都会带 "code" 之类的字段):
res = http.postJSON(url, body);
n = str.len(res);
if (n < 1)
print("网络层失败: 连不上 / 超时 / 非200");
else
ok = str.contain(res, `"code":0`);
if (ok)
print("业务成功");
else
print("业务失败:", res);
end
end建议:一个请求一个变量
-- 每个请求用自己的变量, 好读, 也不会互相串
st = http.get(url_status);
nst = str.len(st);
if (nst < 1)
print("查状态失败");
end历史提醒:2026-08-24 之前的版本有个缺陷 —— 同一个变量被反复赋成长短不一的字符串之后,
某次调用返回 nil 时可能读回它更早的一个旧值,于是失败被当成成功。
该缺陷已修(sKV,bugList.mdBUG-33),当前版本不用再为此绕路;
如果你的现场固件还是旧版本,就按"一个请求一个变量"来写。
全局变量(_ 开头)可以直接接返回值
_token = http.get(url); -- ✓ 可以
_n = str.len(s); -- ✓ 整数也可以全局变量是所有脚本、所有线程共享的,拿来缓存 token 很方便:
n = str.len(_token);
if (n < 1)
_token = api.login(host, "admin", "1234"); -- 只有第一次才去登录
end调用失败时(返回 nil),全局变量会被删掉,不会留着上一次的旧值。
历史提醒:2026-08-24 之前的版本里,_xxx = <任何返回字符串的函数>是静默失效的
—— 值写进了一个同名的局部变量,而读_xxx读的是全局区,所以永远读不到。
该缺陷已修(SimENG的se_return_string()/se_return_nil(),bugList.mdBUG-34)。
如果你的现场固件还是旧版本,就先落到普通变量再转存:s = http.get(url); _token = s;
重试
网络设备偶尔丢一次包很正常,关键动作建议重试:
i = 0;
ok = 0;
while (i < 3)
res = http.postJSON("http://192.168.2.100/api/power", `{"power":"on"}`);
n = str.len(res);
if (n > 0)
ok = 1;
i = 3; -- 成功了就跳出(把计数直接顶到上限)
else
msleep(300); -- 失败等 300ms 再来
end
i++;
end
print("最终结果 ok=", ok);5. JSON 字符串怎么写 ★
JSON 里全是双引号,而脚本的字符串也用双引号,所以有两种写法:
-- 写法一:反引号(推荐)—— 里面的内容原样使用,不做任何转义处理
j = `{"user":"admin","pass":"1234"}`;
-- 写法二:双引号 + 反斜杠转义
j = "{\"user\":\"admin\",\"pass\":\"1234\"}";两种写出来的内容完全一样(实测长度都是 30,str.cmp 返回 1),
但反引号明显好读、不容易漏一个反斜杠,建议一律用反引号。
用变量拼 JSON
vol = 30;
name = "主会议室";
-- str.cat 把所有参数接起来,整数会自动转成字符串
body = str.cat(`{"name":"`, name, `","volume":`, vol, `}`);
-- 得到: {"name":"主会议室","volume":30}
res = http.put("http://192.168.2.100/api/device/1", body);str.cat拼整数时是紧挨着拼的({"volume":30}),不会多出空格,可以放心拼 JSON。
会加空格的是
6. 从响应里取字段 ★
引擎没有 JSON 解析器,靠字符串函数切。三把刀:
| 函数 | 作用 |
|---|---|
str.contain(串, 子串) | 包含返回 1,否则 0 |
str.After(串, 子串) | 返回子串后面的内容 |
str.Before(串, 子串) | 返回子串前面的内容 |
标准套路是 After 一刀 + Before 一刀:
res = `{"code":0,"power":"on","input":"HDMI1","volume":35}`;
-- 取字符串字段 input
v = str.After(res, `"input":"`); -- v = HDMI1","volume":35}
v = str.Before(v, `"`); -- v = HDMI1
print("input =", v);
-- 取数字字段 volume(后面跟的是 } 不是引号)
v = str.After(res, `"volume":`); -- v = 35}
v = str.Before(v, `}`); -- v = 35
print("volume =", v);
-- 取数字字段(后面跟的是逗号)
v = str.After(res, `"code":`);
v = str.Before(v, `,`);
print("code =", v);要点:
- 切的模式串要带上冒号和引号(
"input":"而不是input),
否则 JSON 里别处出现同名字符就切错了。 - 模式串同样建议用反引号写,省得数反斜杠。
- 字段不存在时,
str.After/str.Before不返回任何值,目标变量保持原值不变。
所以想判断"有没有取到",要先放一个哨兵值,取完再比一下:
v = "-"; -- 哨兵
v = str.After(res, `"token":"`);
v = str.Before(v, `"`);
miss = str.cmp(v, "-");
if (miss)
print("响应里没有 token 字段");
else
print("token =", v);
end⚠ 哨兵不能用空串:v = "";这句在本引擎里是无效赋值,
变量会保持它原来的值(实测v="OLDVALUE"; v="";之后str.len(v)还是 8)。
要清空一个变量,用"-"这类短字符串占位。
7. 完整 REST 实战:登录拿 token → 控制设备 → 轮询状态 ★★
这是最典型的一整套流程,下面这段是逐行实测跑通的。
假设设备/平台提供这样一组接口:
POST /api/login {"user":"admin","pass":"1234"}
-> {"code":0,"msg":"ok","token":"AbC123XyZ","expire":3600}
GET /api/status 需要 Authorization: Bearer <token>
-> {"code":0,"power":"on","input":"HDMI1","volume":35}
PUT /api/device/1 需要 token,整体设置
PATCH/api/device/1 需要 token,改单个字段
DELETE /api/device/1 需要 token脚本:
-- ============ 1. 登录,拿 token ============
host = "http://192.168.2.100";
url = str.cat(host, "/api/login");
body = `{"user":"admin","pass":"1234"}`;
res = http.postJSON(url, body);
n = str.len(res);
if (n < 1)
print("登录请求失败: 连不上或超时");
return;
end
-- 先看业务码
ok = str.contain(res, `"code":0`);
if (ok < 1)
print("登录被拒绝:", res);
return;
end
-- 切出 token(用 "-" 当哨兵,空串赋值在本引擎里是无效的)
token = "-";
token = str.After(res, `"token":"`);
token = str.Before(token, `"`);
miss = str.cmp(token, "-");
if (miss)
print("响应里没有 token 字段:", res);
return;
end
print("token =", token);
-- ============ 2. 带上 token,查状态 ============
auth = str.cat("Bearer ", token);
http.clear_headers();
http.set_header("Authorization", auth);
url2 = str.cat(host, "/api/status");
st = http.get(url2); -- 每个请求用自己的变量
n = str.len(st);
if (n < 1)
print("查状态失败");
else
print("状态:", st);
pw = str.After(st, `"power":"`);
pw = str.Before(pw, `"`);
vol = str.After(st, `"volume":`);
vol = str.Before(vol, `}`);
print("电源 =", pw, " 音量 =", vol);
end
-- ============ 3. 控制:整体设置 / 改单个字段 / 删除 ============
url3 = str.cat(host, "/api/device/1");
p1 = http.put(url3, `{"power":"on","volume":30}`);
print("PUT ->", p1);
p2 = http.patch(url3, `{"volume":25}`);
print("PATCH ->", p2);
p3 = http.delete(url3);
print("DELETE->", p3);
-- ============ 4. 收尾:一定要把头清掉 ============
http.clear_headers();实测输出(对着测试服务器):
token = AbC123XyZ
状态: {"code":0,"power":"on","input":"HDMI1","volume":35}
电源 = on 音量 = 35
PUT -> {"code":0,"msg":"ok","method":"PUT","path":"/api/device/1", ...}
PATCH -> {"code":0,"msg":"ok","method":"PATCH","path":"/api/device/1", ...}
DELETE-> {"code":0,"msg":"ok","method":"DELETE","path":"/api/device/1", ...}轮询状态(配合 while)
http.set_header("Authorization", auth);
i = 0;
while (i < 10)
pol = http.get("http://192.168.2.100/api/status");
np = str.len(pol);
if (np > 0)
pw = str.After(pol, `"power":"`);
pw = str.Before(pw, `"`);
same = str.cmp(pw, "on");
if (same)
print("设备已开机,退出轮询");
i = 10;
end
end
sleep(2);
i++;
end
http.clear_headers();把它封装成自定义函数(推荐)
同一套鉴权流程通常要在很多脚本里用,写成模块函数最省事
(自定义函数的完整说明见 readme.txt 的 require 一节):
-- ---------- restapi.tsk ----------
function api.login(host, user, pass)
url = str.cat(host, "/api/login");
body = str.cat(`{"user":"`, user, `","pass":"`, pass, `"}`);
res = http.postJSON(url, body);
n = str.len(res);
ret = "";
if (n > 0)
ret = str.After(res, `"token":"`);
ret = str.Before(ret, `"`);
end
endfun
function api.get(host, path, token)
auth = str.cat("Bearer ", token);
http.clear_headers();
http.set_header("Authorization", auth);
url = str.cat(host, path);
ret = http.get(url);
http.clear_headers();
endfun-- ---------- 主脚本 ----------
require("restapi.tsk");
host = "http://192.168.2.100";
token = api.login(host, "admin", "1234"); -- 普通变量可以直接接
st = api.get(host, "/api/status", token);
print(st);函数体里有自己的if/while,所以在while循环里调这种函数是安全的,
不受"if不能嵌套if"那条限制。
8. URL 与查询参数:str.EncodeURL
查询参数里有中文、空格、&、=、#、+ 这些字符时,必须先编码,
否则参数会被服务器截断或切错。
kw = str.EncodeURL("会议室 A&B");
-- kw = %E4%BC%9A%E8%AE%AE%E5%AE%A4+A%26B
url = str.cat("http://192.168.2.100/api/find?room=", kw);
res = http.get(url);编码规则:A-Z a-z 0-9 . - _ * 原样保留,空格变 +,其它一律 %XX。str.EncURL 是同一个函数的短名。
只编码参数值,别把整条 URL 拿去编码 —— 那样://和?也会被编掉。
9. 容量与超时(都能改,改完要重编译)
| 项目 | 值 | 位置 | 说明 |
|---|---|---|---|
| URL 最长 | 255 字节 | SE_HTTP_URL_SIZE | 超长会被截断 |
| 请求体最长 | 2047 字节 | _SE_HTTP_USRBUFFER_SIZE | 超长被截断 |
| 响应最多拿到 | 1024 字节 | _SE_HTTP_RET_SIZE | ★ 超出部分丢弃 |
| 收包缓冲 | 2048 字节 | _SE_HTTP_USRBUFFER_SIZE | 头+体都在里面 |
| 自定义头 | 8 条 × 255 字节 | SE_HTTP_MAX_HEADERS | 超出静默丢弃 |
| 连接超时 | 3 秒 | SE_HTTP_CONN_TIMEOUT_MS | |
| 收包超时 | 5 秒 | SE_HTTP_RECV_TIMEOUT_MS | 两段数据之间的最大间隔 |
| 发送超时 | 5 秒 | SE_HTTP_SEND_TIMEOUT_MS |
最容易踩的是响应 1024 字节这条:接口返回的 JSON 一长,脚本拿到的就是被截断的半截,str.After 自然什么都切不出来。遇到这种情况:
- 优先让服务端提供"精简版"接口,或用查询参数只要需要的字段;
- 实在不行就找工程改大
_SE_HTTP_RET_SIZE(每个脚本线程栈上会多占它的 2 倍字节)。
引擎本身支持的响应体不受此限(chunked 分块、几十 KB 的响应都能正确收完),
1024 是"交给脚本变量"这一步的上限。
10. 排查对照表
| 现象 | 原因 / 处理 |
|---|---|
| 返回一直是空 | 先确认不是 200 以外的状态码(用 curl 手工试一次);再看是不是连不上 |
打印出 http_tcpclient_create failed | 连不上:IP/端口错、设备没开、网络不通 |
打印出 http_tcpclient_recv failed | 服务器收了请求不回包,5 秒超时;确认接口路径对不对 |
打印出 http_parse_url failed | URL 没有 http:// 前缀,或 host 部分超过 255 字节 |
打印出 request header too long | 自定义头加太多/太长,精简一下 |
| 返回的 JSON 明显是半截 | 超过 1024 字节被截了,见第 9 节 |
str.After 切不出东西 | 模式串写错(少了引号/冒号),或响应被截断;先 print(res) 看原文 |
| 换了接口后 401 | 上一段脚本设的 Authorization 还留着,或者忘了设;http.clear_headers() 后重设 |
https:// 的地址用不了 | 不支持 TLS,得走反向代理转成 http |
明明失败了 str.len 却不为 0 | 旧版本(2026-08-24 之前)的 sKV 缺陷,升级引擎;见第 4 节 |
_xxx = http.get(...) 拿不到东西 | 旧版本(2026-08-24 之前)的缺陷,升级引擎;旧固件先给普通变量再转存 |
x = ""; 之后变量还是老值 | 空串赋值在本引擎里无效,用 x = "-"; 这类短字符串占位 |
| 脚本卡住不动 | 单次请求最坏约 3s(连接)+ 5s(收包);while 里连着请求要算好总时间 |
11. 其它注意事项
- 不支持函数嵌套调用。
http.get(str.cat("http://", ip))是不行的,
先url = str.cat(...)再http.get(url)。 require必须写在调用之前,引擎是边读边执行的,没有预扫描。- 多线程:每个脚本线程各有自己的自定义头,互不干扰;
下划线开头的全局变量(_token)则是所有脚本、所有线程共享的,用来缓存 token 很方便:
n = str.len(_token);
if (n < 1)
_token = api.login(host, "admin", "1234"); -- 直接接就行(2026-08-24 之后的版本)
end
print("token:", _token);- HTTP 响应的分块传输(chunked)引擎已经自动解好,脚本拿到的就是干净的正文,
不会看到1a\r\n....这种块长度标记。 - keep-alive 的服务器不会拖慢请求:引擎按
Content-Length/ 块结束标记判断收完就返回,
不会傻等对端关闭连接。



0 条评论