TCP 字节写入协议回执设备动作和实物结果四层边界

Code=1 证明字节写入,不替设备动作和实物结果作保证。

摘要| 我沿 Microi吾码AI 当前 V8.Tcp 实现,从接口引擎的可信设备目标、Bytes/Hex/Text 编码,追到一次性 TCP 连接、有界收发、协议回执和业务终态。结论:Code=1 只证明字节已经写入 TCP 连接;要证明设备接受、机械动作和小票出纸,必须继续读取协议回执、设备状态或现场证据。本文绑定当前源码行号、SHA-256 与 8/8 聚焦测试。

① 后端说成功,小票机为什么一动不动

设备接入最常见的误判,是把 `V8.Tcp.Send` 返回 `Code=1` 直接翻译成“小票已打印”。实际上,当前返回的数据只有 `BytesSent` 和 `RemoteEndPoint`:它证明一次 TCP 连接完成了字节写入,却不知道打印机是否解析了 ESC/POS 命令、纸仓是否缺纸、刀头是否卡住,更不知道顾客手里是否真的拿到小票。

网络写入、协议接受、设备动作和实物结果,是四个不同的成功边界。把它们混成一个绿色对勾,超时重试时就可能连续出两张票。

可信配置协议帧TCP写入设备回执和业务终态调用链

V8.Tcp 负责有界 I/O;协议与业务完成语义仍要由接口引擎编排。

先给结论|| Code=1 到网络为止;设备是否执行,要靠设备协议或更高层证据继续证明。

② V8.Tcp 适合什么,不适合什么

`V8.Tcp` 是 Microi吾码AI 后端的一次性原始字节客户端,适合 RAW/JetDirect 9100 小票机、串口服务器、PLC 或私有 TCP 协议。它公开同步和异步的发送、收发方法,每次调用建立连接、执行有界 I/O 后关闭,不向 V8 暴露持久 Socket。

  • HTTP/HTTPS 接口应使用 `V8.Http`,不要用 TCP 重写 HTTP。
  • RabbitMQ/MQTT 使用 `V8.MQ` 或 MQTT 引擎,不要混用协议层。
  • 长连接、TLS、TCP 服务端监听或自定义连接池,应设计独立网关/Worker。
  • 设备选择、票据模板、幂等、审计和状态机继续放在接口引擎与业务表。

平台边界|| V8.Tcp 是安全受限的原子 I/O 能力,不是一套设备管理系统。

③ 先把目标锁死,再谈发什么字节

TCP 不经过 `V8.Http` 的 SSRF 防护。如果把 `Host`、`Port` 从 `V8.Param` 原样传入,普通调用者就获得了扫描后端可达内网的能力。生产实现应从 SaaS 可信配置或受行权限保护的设备表读取目标,再按当前租户、门店和用户权限命中精确白名单。

// device 必须来自可信配置/受权限保护的设备表
var device = V8.FormEngine.GetFormData('device_terminal', {
  _Where: [['Code', '=', V8.Param.deviceCode], ['Enabled', '=', 1]],
  _SelectFields: ['Host', 'Port']
});
if (device.Code !== 1) return { Code: 0, Msg: '设备不可用' };

// 禁止 Host: V8.Param.Host / Port: V8.Param.Port
var host = device.Data.Host;
var port = device.Data.Port;

安全底线|| 调用者选择的是已授权设备编号,不是任意 IP 与端口。

④ 一个完整帧,只能选择一种载荷

当前实现允许 `Bytes`、`ByteBase64`、`Hex`、`Text` 四种表达,但一次调用必须且只能提供一种。控制命令、正文、换行、切纸和校验码要先组成完整协议帧,再打开一次连接发送。中文小票可以使用 `Text + Encoding: 'gb18030'`;混合控制字节时,更适合提前编码成 Bytes、Hex 或 Base64。

var result = V8.Tcp.Send({
  Host: host,
  Port: port,
  Bytes: [27, 64, 77, 105, 99, 114, 111, 105, 10, 29, 86, 0],
  ConnectTimeout: 5,
  SendTimeout: 5
});
if (result.Code !== 1) return result;
// 此处只能记录 tcp_written,不能记录 printed。

源码把连接、发送和接收超时限制在 1~120 秒,单次发送硬上限 4 MiB,默认最多读取 64 KiB,绝对接收上限 1 MiB。硬上限是防止 V8 脚本把一次设备调用变成无限内存或无限等待。

⑤ 需要证明设备接受,就显式读取协议回执

TCP写入协议接受设备动作和实物结果四种成功语义

每一层都应拥有独立状态与证据。

设备协议定义 ACK、状态字或响应帧时,应使用 `SendAndReceive`。返回值包含 `RawBytes`、`Hex`、`ByteBase64`、`BytesReceived`、`ReceiveEndReason` 和 `Truncated`,接口引擎必须按协议长度、校验码和状态位解析,不能因为收到任意一个字节就宣布成功。

var reply = V8.Tcp.SendAndReceive({
  Host: host, Port: port,
  Hex: '01 03 00 00 00 02 C4 0B',
  ReceiveTimeout: 3,
  MaxReceiveBytes: 4096
});
if (reply.Code !== 1) return reply;
if (reply.Data.Truncated || reply.Data.ReceiveEndReason === 'Timeout')
  return { Code: 0, Msg: '响应不完整,进入结果未知态' };
// 下一步:按设备协议校验长度、CRC 和状态码。

当前接收语义很细:零字节直到超时会失败;已经收到部分数据后超时,会返回成功和已有字节,并标记 `ReceiveEndReason='Timeout'`;达到上限则标记 `MaxReceiveBytes`。因此 `Code=1` 也不自动等于完整响应。

⑥ 超时后最危险的动作,是立刻再打一遍

打印、开闸、继电器动作都不是天然幂等。发送超时可能发生在字节尚未到达,也可能发生在设备已经执行但响应丢失。把这两种情况都当失败并自动重试,会制造重复出纸或重复动作。

CommandId outbox 发送中回读确认和终态对账流程

结果未知必须成为正式状态,不能被代码顺手折叠成失败。

  1. 为每条设备命令生成稳定 CommandId,并在数据库记录待发送。
  2. 用 outbox、Job 或 MQ 驱动后台发送,不把长 I/O 留在前端请求里。
  3. 分别记录 tcp_written、device_acknowledged、action_confirmed、unknown。
  4. 结果未知时优先查询设备状态、回读序号或人工确认,再决定是否重试。
  5. 日志只留设备标识、业务单号、字节数、耗时和错误分类,不落票据秘密。

⑦ 这次真正验证了哪些能力

V8Tcp八项聚焦测试全部通过

当前本地 V8.Tcp 聚焦测试 8/8 通过,失败为 0。

我在当前已编译的 Microi.Tests 程序集上执行 `V8TcpTests`:8 项、8 项通过、0 项失败。测试不是只断言方法名,而是真实启动本机 TCP listener,经过 Jint 注入调用 V8.Tcp,核对字节收发;还覆盖 Hex、Base64、GB18030、歧义载荷拒绝、4 MiB 上限、部分超时和响应截断。

这证明当前实现与回环测试合同一致。它没有连接真实打印机、PLC 或串口服务器,也没有证明生产 Docker 到设备网段的路由、防火墙和白名单,更不能替代现场出纸验收。

验证边界|| 回环测试证明原始字节 I/O;真实设备协议、机械动作与实物结果必须单独验收。

⑧ 结尾:设备接入最值钱的是诚实的完成语义

V8.Tcp 把一次性连接、四类载荷、有界超时、有界接收和多种返回格式封装成了接口引擎可用的原子能力。平台底座解决的是“安全、可控地发和收”,业务系统还要继续解决“发给谁、哪条命令、设备是否接受、动作是否完成、未知结果如何恢复”。

下一次看到 `Code=1`,不要急着弹出“打印成功”。先问:这是字节写入、协议确认、设备动作,还是实物结果?能证明到哪一层,系统就只承诺到哪一层。这一个看似保守的原则,往往能省掉最多的重复打印、错账和现场扯皮。

本文验证范围|| 当前源码 SHA、行号与 2026-08-23 的 8/8 本地聚焦测试成立;未把回环测试冒充真实设备出纸。

Logo

宁波官方开源宣传和活动阵地,欢迎各位和我们共建开源生态体系!

更多推荐