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% 的坑:

  1. 返回的是"响应体",不含响应头;请求失败或服务器不是 200 时返回 nil(空)。
  2. JSON 用反引号 ` 包起来,里面的双引号不用转义(见第 5 节)。
  3. 引擎不支持函数套函数: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();                                -- 用完清掉

必须知道的四条规则:

  1. 设了就一直有效,直到调用 http.clear_headers() 或脚本结束。
    不清掉的话,后面每一个 http.* 请求都会带上它。
  2. 对所有方法都生效,get / post / postJSON / put / patch / delete 一视同仁。
  3. 最多 8 条,超出的会被静默丢弃;单条的 key 和 value 各最长 255 字节。
  4. 是"追加",不是"替换"。设 Content-Type 这种内置就有的头,请求里会出现两条,
    多数服务器取后一条,但不保证 —— 尽量别去覆盖 Host / Content-Type / Content-Length。
自定义头是每个脚本线程各自一份的:A 脚本设的头不会影响同时在跑的 B 脚本。
反过来说,同一个脚本里设了就得自己清。

4. 返回值与错误处理

成功:返回响应体字符串(不含响应头)。
失败:返回 nil,此时目标变量是空的。

以下情况都算失败,返回 nil:

情况说明
连不上(地址错 / 端口没开 / 网线断)3 秒内返回
服务器不回包5 秒收包超时后返回
响应状态码不是 200404 / 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.md BUG-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.md BUG-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。
会加空格的是 print(它给整数两边各留一个空格),那只影响日志好看不好看。

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 failedURL 没有 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 / 块结束标记判断收完就返回,
    不会傻等对端关闭连接。