模拟器
模拟器就是由 Signal Lab 扮演的、您的系统所对接的 API、设备或服务。它监听一个地址,并按规则应答:HTTP API 按路由应答,OSC、UDP 或 TCP 设备按“收到这个,就回复那个”应答,MQTT 代理则像任何代理一样工作,另外还有自己的规则。它可以变慢、出错,或时不时停机,以便测试依赖出现异常时您的系统会怎样。每次交互都会被计数、列出,并发送到检查器。
一个模拟器就是一个文档。模拟器 界面维护一个模拟器库;同一个文档可以作为 模拟器 节点在实验中运行,可以通过命令行 signallab emulate 运行,也可以通过 API 和 MCP 运行,在哪里应答方式都一样。
界面
左侧是库(库):每个模拟器及其协议和地址,正在运行的模拟器带有一个跳动的圆点和请求计数。右侧是所选模拟器的设置和规则,其下方是它收到的内容(实时)。
创建模拟器
按库顶部的某个按钮:
按钮 创建 监听于 自带一条可直接使用的规则 + HTTP API HTTP API 127.0.0.1:18080GET /health→ 200{"status":"ok"}+ OSC 设备 OSC 设备 127.0.0.1:9100/ping→/pong,附带 int 类型的计数+ UDP 设备 UDP 设备 127.0.0.1:7100包含 PING的数据报 →PONG 1、PONG 2、…+ TCP 设备 TCP 设备 127.0.0.1:7200包含 PING的一行 →PONG+ MQTT 代理 MQTT 代理 127.0.0.1:1883发布到 lab/<name>/set的消息 → 在lab/<name>/state上以保留消息的形式发布相同的载荷如果库中已有其他模拟器使用该端口,则改用下一个空闲端口。
填写 名称(最多 120 个字符)。
设置 监听于:
IP:port。127.0.0.1只应答本机;0.0.0.0也应答网络中的其他机器。修改规则(见下文),并在 备注 中写明它所代替的对象。
更改会自动保存。创建副本 会在下一个空闲端口上创建一个副本。删除 会再确认一次(删除?),如果模拟器正在运行就停止它,然后将其从库中移除。
规则按从前到后的顺序尝试;第一条匹配的规则进行应答。每条规则的标题栏显示一行摘要;点击它即可展开或折叠该规则。↑ 和 ↓ 按钮用于移动规则,× 用于移除规则。
运行
- 选择模拟器,按 启动。在按钮恢复之前,其端口就已打开:如果端口已被占用,或模拟器存在问题,会在这一步被拒绝并给出原因。
- 将您的系统指向它。对于 HTTP API,复制 URL 会复制其地址(
http://127.0.0.1:18080),每个路由也都有一个 复制 URL 按钮,用于复制该路由自己的地址(路径中含有{{…}}模板时除外)。 - 观察 已接收 逐渐填满。
- 按 停止,或在控制台的任务条中停止它的任务。
按钮旁边的状态显示 未运行、它在哪里应答,或者它已停机。
模拟器会一直按启动时的规则应答。如果在它运行时修改了它,会出现 重启:按下它,即可按当前的规则重新启动。在此之前,规则上的命中次数会被隐藏,因为它们属于旧的规则。
停机 会让正在运行的模拟器不可用,直到您按下 恢复:HTTP 请求会得到 503,TCP 设备和 MQTT 代理会断开现有连接并拒绝新连接,OSC 或 UDP 设备则不作任何应答。参见停机。
同一种传输协议的两个模拟器不能共用一个端口:HTTP、TCP 和 MQTT 模拟器监听 TCP 端口,OSC 和 UDP 模拟器监听 UDP 端口。一个 HTTP API 和一个 OSC 设备可以都使用端口 8080;两个 HTTP API 则不行。在已被占用的端口上启动第二个模拟器时会被拒绝。
TIP
在连接到服务器的浏览器中,模拟器运行在服务器上。监听 0.0.0.0 的模拟器通过服务器的名称访问,复制 URL 复制的就是这个地址;监听 127.0.0.1 的模拟器只应答服务器自身上的程序。
收到的内容
运行期间,实时 会统计:
| 计数 | 内容 |
|---|---|
| 请求 | 收到的一切:请求、消息、行。 |
| 无规则 | 没有任何规则接收的内容。没有路由的 HTTP 请求仍会得到应答(参见无路由接收的请求);其他协议则不应答。 |
| 失败 | 无法生成或发送回复的交互。 |
| 停机期间 | 模拟器停机期间收到的内容。设置了停机计划,或有内容在停机时到达时才显示。永远不计入 无规则。 |
| 未送达 | 仅限 MQTT,发生时才显示:客户端落后太多而无法接收的消息。 |
每条规则的标题栏显示自启动以来它匹配的次数。
已接收 列出最新的 300 次交互,最新的在最前:
| 列 | 内容 |
|---|---|
| 时间 | 到达的时间。 |
| 来源 | 客户端的地址。 |
| 请求 | 收到的内容,以协议记法表示:GET /users/7、/ping 1、POWER?。 |
| 规则 | 接收它的规则(#2),或 —。 |
| 回复 | 返回的内容:200 OK · 37 B、/pong 3、一段载荷;故障时为 已挂起 或 已关闭;回复失败时为错误;在停机期间到达时为 停机。 |
| ms | 从到达到回复发出的时间,包括其延时。 |
行上的 ⌕ 按钮(在检查器中打开)会在检查器中打开该交互(前提是当时捕获已开启)。当五分之一秒内到达的交互超过 200 次时,列表会跳过一部分,并注明跳过了多少。引擎会为每个正在运行的模拟器保留最新的 500 次交互及其收到的内容,供命令行、API 和 MCP 使用。
HTTP API
一个 HTTP/1.1 服务器。每个请求由第一个接收它的路由应答。
路由
当请求的方法、路径和所有条件都匹配时,路由就会接收该请求。
| 字段 | 内容 |
|---|---|
| 方法 | GET、POST、PUT、PATCH、DELETE、HEAD、OPTIONS,或 任意。GET 路由也会应答 HEAD。 |
| 路径 | 以 / 开头。路径段 :name 匹配任意一个路径段,以 {{request.params.name}} 读取;最后一段为 * 时匹配其下的所有内容。末尾的 / 没有影响;查询字符串不属于路径。 |
| 条件 | 每一条都必须满足。用 + 条件 添加。 |
路径示例:
| 路径 | 匹配 | 不匹配 |
|---|---|---|
/health | /health、/health/ | /health/db、/Health |
/users/:id | /users/7(params.id 为 7)、/users/a%20b(a b) | /users、/users/7/orders |
/files/* | /files、/files/a、/files/a/b/c | /file、/other/files/a |
条件读取请求的某一部分(位置),并进行比较:
| 位置 | 名称 | 读取 |
|---|---|---|
| 标头 | 标头名称,不区分大小写 | 该标头的值;标头发送了多次时,各值以 , 连接。 |
| 查询 | 查询参数 | 解码后的值;重复出现时取第一个。 |
| 正文 | — | 整个正文,作为文本。 |
| JSON | JSON 路径,例如 $.user.id | JSON 正文中的该字段。 |
比较方式有 等于、不等于、小于、不大于、大于、不小于、包含、匹配正则、为空 和 不为空。数字按数值比较,文本精确比较。不存在的标头、参数或字段视为空。无法进行的比较(例如文本与数字比较)不成立。
响应
一个路由有 1 到 16 个响应(响应)。
| 字段 | 内容 | 默认值 |
|---|---|---|
| 状态 | 100–599。 | 200 |
| 故障 | 不返回正常应答,而是出现其他情况;参见故障。 | 无——正常应答 |
| 延时(ms) | 应答前等待多久,0–60,000 ms。 | 0 |
| 抖动(ms) | 随机延长至多此值,0–60,000 ms。 | 0 |
| 权重 | 路由随机应答时它所占的比重。仅在这种情况下显示。 | 1 |
| 标头 | 最多 32 个。名称可以使用参数;值是模板。 | 无 |
| 正文 | 一个模板,按书写的内容计算最多 256 KiB。 | 空 |
没有 Content-Type 标头时,有效 JSON 的正文以 application/json 发送,其他正文以 text/plain; charset=utf-8 发送。
有两个或更多响应时,响应选择 决定请求得到哪一个:
| 响应选择 | 请求得到 | 用途 |
|---|---|---|
| 依次,之后保持最后一个 | 第一个、第二个……此后一直是最后一个:500、500、200、200、200…… | 重试:失败两次,然后成功。 |
| 轮流 | 最后一个之后再回到第一个:200、500、200、500…… | 有规律地时而失败的依赖。 |
| 按权重随机 | 每次按权重抽取。权重为 8 和 2 时,第一个约占 80% 的次数。至少有一个权重必须大于 0。 | 符合实际的失败比例。 |
添加响应 会为路由添加一个现成的响应:
| 预设 | 添加 |
|---|---|
| 200 JSON | 200,{"ok":true} |
| 201 已创建 | 201,{"id":"{{uuid}}"},标头 Location: {{request.path}}/{{counter}} |
| 404 未找到 | 404,{"error":"not found"} |
| 500 服务器错误 | 500,{"error":"internal"} |
| 503 不可用 | 503,{"error":"unavailable"},标头 Retry-After: 1 |
| 慢速——2 s | 2000 ms 后返回 200,{"ok":true} |
| 无应答 | 故障 无应答 |
| 连接关闭 | 故障 关闭连接 |
| 格式错误的 JSON | 200,{"items":[{"id":1},{"id":2}]},带故障 格式错误的正文 |
故障
| 故障 | 客户端遇到的情况 |
|---|---|
| 无——正常应答 | 正常的响应。 |
| 无应答 | 什么都没有。请求最多被挂起 2 分钟,然后连接关闭——因此测试的是客户端自己的超时。延时不适用。 |
| 关闭连接 | 经过延时后,连接在没有应答的情况下关闭。 |
| 格式错误的正文 | 一个完整的 HTTP 应答,带有设定的状态和标头,但正文在中途截断:无法解析的 JSON。如果整个正文原本是 JSON,内容类型仍为 application/json。 |
无路由接收的请求
无路由接收的请求 决定不匹配任何路由的请求会得到什么:
- 404 未找到——404,正文为
{"error":"no_route"}; - 此应答——您设置的响应,具备路由响应的全部内容。其中的
{{counter}}统计没有路由接收的请求数。
无论哪种方式,该请求都计为 无规则。
HTTP 回复可以读取的内容
| 模板 | 含义 |
|---|---|
{{request.method}} | GET、POST、… |
{{request.path}} | 路径,不含查询字符串。 |
{{request.params.id}} | 名为 :id 的路径段。 |
{{request.query.page}} | 解码后的查询参数。 |
{{request.headers.x-key}} | 一个标头;名称为小写。 |
{{request.body}} | 正文文本:其前 64 KiB。 |
{{request.json.name}} | JSON 正文中的一个字段,前提是正文为 JSON 且不超过 64 KiB。 |
{{request.from}} | 客户端的 IP:port。 |
大于 1 MiB 的请求正文会得到 413,并计为 失败。无法生成的回复(模板引用了请求中没有的内容)会得到 500,错误信息在其正文中,并计为 失败。
OSC 设备
到达的每条消息(消息包中的每条消息各自单独处理)由它匹配的第一条规则应答。不是 OSC 的数据报计为 无规则。
| 字段 | 内容 |
|---|---|
| 地址模式 | OSC 1.0 地址模式:* 任意字符,? 一个字符,[a-z] 字符集,{a,b} 二选一,均限于一个路径段内(参见 OSC)。 |
| 参数规则 | 最多 16 个针对参数的条件,与 等待 OSC 中相同(参见节点)。 |
| 回复 | 关闭时:接收消息,不作任何应答。 |
| 回复地址 | 回复的地址,一个模板。 |
| 回复参数 | 最多 16 个参数,每个包括 类型(int、float、str、long、double、bool、blob、nil)和一个 值 模板。 |
| 回复到 | 留空:回复到发送方的地址和端口。否则填写 IP:port。 |
| 延时(ms)、抖动(ms) | 各为 0–60,000 ms。 |
填好模板后,参数的值会按其类型读取:类型为数值时,{{request.args[0]}} 会以数字形式回显第一个参数。bool 接受 true、1、yes、on 或 false、0、no、off;blob 接受十六进制字节;空值即该类型的零值。
回复从模拟器自己的端口发出,因此在发送端口上监听的客户端能收到它们。
OSC 回复可以读取 {{request.address}}、{{request.args[0]}} 和 {{request.from}}。
UDP 设备
每个数据报由它匹配的第一条规则应答。
| 字段 | 内容 |
|---|---|
| 匹配 | 任意数据报、包含文本、匹配正则 或 包含字节(hex)。 |
| 模式 | 要查找的文本、正则表达式或字节。 |
| 回复 | 接收但不回复、文本 或 Hex,然后是回复本身,一个模板。 |
| 回复到 | 留空:回复给发送方。否则填写 IP:port。 |
| 延时(ms)、抖动(ms) | 各为 0–60,000 ms。 |
UDP 或 TCP 回复可以读取:
| 模板 | 含义 |
|---|---|
{{request.text}} | 载荷文本。 |
{{request.match}} | 匹配到的内容:文本、正则表达式的第一个分组(或整个匹配)、字节。 |
{{request.hex}} | 以十六进制字节表示的载荷,取前 1024 个字节。 |
{{request.bytes}} | 载荷的大小。 |
{{request.from}} | 发送方的 IP:port。 |
文本回复最多 65,507 字节。
TCP 设备
一种在 TCP 连接上按行通信的设备,例如投影机或矩阵切换器。客户端发送的每条消息由它匹配的第一条规则应答;回复通过同一连接返回。
| 字段 | 内容 |
|---|---|
| 消息结束符 | 标志一条消息结束的字符,也会添加在每条回复和问候语之后:LF (\n)(其前的 \r 会被去掉)、CR LF (\r\n)、CR (\r) 或 无——按每个数据块。空行会被跳过。 |
| 问候语 | 客户端连接时发送;留空则不发送。可以读取 {{request.from}}。 |
| 匹配、模式、回复 | 与 UDP 设备相同。 |
| 然后关闭连接 | 在此规则的回复之后关闭连接,例如对 QUIT。 |
| 延时(ms)、抖动(ms) | 各为 0–60,000 ms。 |
超过 64 KiB 仍没有结束符的消息,会按其现状接收。
MQTT 代理
一个基于明文 TCP 的小型 MQTT 3.1.1 代理。它做代理该做的事:客户端连接,用 + 和 # 订阅,以 QoS 0、1 和 2 发布,保留消息和遗嘱消息都有效,使用同一客户端 ID 的第二个连接会取代第一个。会话总是全新的:要求保留会话的客户端会得到一个新会话,也不会为不在线的客户端排队任何内容。
除此之外,发布给它的每条消息都会与规则进行比对:第一条匹配的规则还会发布一条回复——就像设备在报告自己做了什么。
| 字段 | 内容 |
|---|---|
| 用户名、密码 | 设置了用户名时,客户端必须用该用户名和密码连接;留空:任何人都可以连接。只有密码而没有用户名会被拒绝,因为 MQTT 3.1.1 无法单独携带密码。 |
| 保留消息 | 最多 64 条消息(主题、载荷、QoS),从启动起即持有,如同以 retain 发布:订阅的客户端会首先收到它们。 |
| 主题过滤器 | 规则接收哪些主题:+ 匹配一级,# 匹配其余部分,例如 lab/+/set。 |
| 匹配、模式 | 针对载荷的条件,与 UDP 设备相同。 |
| 回复 | 关闭时:接收消息,不再发布任何内容。 |
| 回复主题、回复载荷 | 模板。主题中不能含有 + 或 #。 |
| QoS、Retain | 回复的设置。 |
| 延时(ms)、抖动(ms) | 各为 0–60,000 ms。 |
MQTT 回复可以读取 {{request.topic}}、{{request.levels[1]}}(主题的各级,从 0 开始)、{{request.payload}}、{{request.json.state}}、{{request.match}}、{{request.qos}}、{{request.retain}}、{{request.client}}(客户端 ID)和 {{request.from}}。
回复中的模板
回复使用与实验相同的模板语言编写,因此一个字段在这里和在实验中含义相同。回复可以读取:
request——收到的内容,见上文各协议的列表;{{counter}}——自模拟器启动以来此规则接收的消息数,包括本条;- 生成器——
{{uuid}}、{{now.iso}}、随机值等;随机值从模拟器的种子中抽取; - 参数,当模拟器在实验中运行,或用
signallab emulate --param启动时。
回复从不读取机密;未知名称会报错,而不是得到空文本。
有些字段在模拟器启动时、任何内容到达之前就已确定:路径、条件、地址模式、载荷模式、主题过滤器、回复到、标头名称、保留消息以及代理的登录信息。它们只接受文本和参数,不接受 request,也不接受生成器。
种子决定响应的随机顺序、抖动和随机生成器。在 模拟器 界面上,每次启动都会使用新的种子;实验使用本次运行的种子,signallab emulate --seed 则使用您给定的种子。
停机
要测试依赖时断时续时您的系统会怎样,请勾选 间歇性停机:
| 字段 | 内容 | 默认值 |
|---|---|---|
| 在线时长(ms) | 应答的时长,10–3,600,000 ms。 | 10,000 |
| 停机时长(ms) | 停机的时长,10–3,600,000 ms。 | 3000 |
| 停机期间 | 仅限 HTTP:停机期间请求会遇到什么。 | 503 不可用 |
停机计划从模拟器启动时开始,并不断重复:在线、停机、在线、停机……停机期间:
| 模拟器 | 遇到的情况 |
|---|---|
| HTTP | 503 不可用:503,Retry-After 设为距恢复还剩的秒数(至少为 1)。关闭连接:连接在没有应答的情况下关闭。无应答:最多挂起 2 分钟,然后关闭。 |
| TCP 设备 | 已打开的连接在 0.1 s 内断开;新连接一到达就被关闭。 |
| MQTT 代理 | 所有连接都会断开;新连接会被拒绝(CONNACK 返回码 3,服务器不可用)。 |
| OSC、UDP 设备 | 不作任何应答。 |
停机期间到达的内容计为 停机期间,而不是 无规则,也不会交给规则处理。
停机 可以随时手动实现同样的效果,无论计划如何,直到您按下 恢复;此时 HTTP 得到的 503 不带 Retry-After。在实验中,模拟器停机/恢复 节点会在运行的某个步骤执行这一操作(参见节点和故障)。
问题
编辑时,每次更改后片刻就会检查模拟器;在您按 启动 之前,问题就会显示在按钮下方。问题会指出所在位置(规则、响应或保留消息,以及字段)和出错的内容:缺少 / 的路径、无法编译的正则表达式、引用了 request、参数和生成器以外内容的回复模板、超出范围的值。启动 会拒绝启动存在问题的模拟器。
限制
| 项目 | 限制 | 达到限制时 |
|---|---|---|
| 每个模拟器的路由或规则 | 64 | 检查时被拒绝。 |
| 每个路由的响应 | 16 | 被拒绝。 |
| 每个路由的条件 | 16 | 被拒绝。 |
| 每个响应的标头 | 32 | 被拒绝。 |
| 参数条件、回复参数(OSC) | 各 16 | 被拒绝。 |
| 保留消息(MQTT) | 64 | 被拒绝。 |
| 按书写的内容计算的正文、回复或问候语 | 256 KiB | 被拒绝。 |
| 延时或抖动 | 60,000 ms | 被拒绝。 |
| HTTP 请求正文 | 1 MiB | 413。 |
| 同时存在的 HTTP 连接 | 512 | 超出的连接一到达就被关闭。 |
| HTTP 请求头 | 30 s | 客户端必须在此时间内发送完。 |
| 同时存在的 TCP 连接 | 256 | 超出的连接一到达就被关闭。 |
| 等待延时的 OSC 和 UDP 回复 | 1024 | 超出的回复会被丢弃,并计为 失败。 |
| 同时连接的 MQTT 客户端 | 256 | 超出的客户端一到达就被关闭。 |
| MQTT 数据包 | 256 KiB | 该客户端的连接结束。 |
| 每个客户端的 MQTT 订阅 | 100 | 超出的订阅会被拒绝。 |
| MQTT 保留主题 | 1000 个主题,16 MiB | 新的保留消息会被路由,但不会被保留。 |
| 为一个慢速客户端排队等待的 MQTT 消息 | 1024 条消息,8 MiB | 该客户端会错过它们;计为 未送达。 |
从响应创建模拟器
要根据一个成功的响应创建模拟器:
- 在 HTTP 界面上,发送请求并得到响应——或在实验的 HTTP 节点上使用 立即发送。
- 按响应旁边的 ⧉ 据此模拟。模拟此响应 对话框会显示它将创建的路由。
- 在 添加到 中,选择您的某个 HTTP 模拟器,或选择 新模拟器。
- 按 添加路由。模拟器 界面会打开并定位到该模拟器。
该路由以响应的状态、标头和正文来应答该请求的方法和路径(不含查询字符串)。只属于那一次交互的标头(Content-Length、Date、Server、ETag 等)会被去掉,正文按原样发送,即使其中含有 {{。新模拟器只包含这一个路由。添加到现有模拟器时,该路由会放在最前面,以便在更宽泛的路由之前应答;正在运行的模拟器会在您按 重启 后采用它。
从实验创建时,用模板写的 URL 会变成模式:其基础部分({{api}})会被去掉,整段为一个模板的路径段(/orders/{{order_id}})会变成 :order_id,只有部分使用模板的路径段则会以 * 结束路径。
初始模拟器
Signal Lab 第一次找不到模拟器库时,会写入五个模拟器,都在本机上。它们的名称和备注以当时的界面语言写成。
| 模拟器 | 监听于 | 作用 |
|---|---|---|
| 演示 API | 127.0.0.1:8080 | GET /health → {"status":"ok","time":…};GET /users/:id → 该 id 的用户;POST /users → 201,带 Location;GET /slow → 1500 ms 后应答;/flaky → 503、503,此后一直是 200。 |
| 演示 OSC 设备 | 127.0.0.1:9100 | /ping → /pong,附带计数;/fader/* → /ack,附带收到的地址;/cue/* 接收但不回复。 |
| 演示 UDP 设备 | 127.0.0.1:7100 | PING → PONG 和计数;其他任何内容 → ACK 及其字节大小。 |
| 演示 TCP 设备 | 127.0.0.1:7200 | 以 CR LF 结尾的行。以 READY 问候;POWER? → POWER=ON;POWER ON 或 POWER OFF → OK ON / OK OFF;QUIT → BYE,然后挂断。 |
| 演示 MQTT 代理 | 127.0.0.1:1883 | 在 lab/status 上保留 online;发布到 lab/<name>/set 的 ON 或 OFF → 在 lab/<name>/state 上以保留消息的形式发布相同内容。 |
信号库中的初始信号 服务是否在线? 会请求 http://127.0.0.1:8080/,即 演示 API 的地址:它没有 / 的路由,因此会得到 404。
库文件
库是数据文件夹中的 emulators.json(参见文件);将指针悬停在列表下方的计数上可以看到其路径。它会在最后一次更改 0.7 s 后通过临时文件整体写入,因此写入失败时会保留上一个版本。如果文件无法读取,列表会显示错误以及路径、行和列,文件保持原样:修复它,然后按 重新加载文件。手动编辑文件后,也请按 重新加载文件。没有文件时,会重新写入初始模拟器。
{
"version": 1,
"emulators": [
{
"id": "orders-api",
"note": "Stands in for the orders service.",
"emulator": {
"name": "Orders API",
"bind": "127.0.0.1:18080",
"protocol": "http",
"routes": [
{ "method": "GET", "path": "/orders/:id",
"responses": [{ "body": "{\"id\":\"{{request.params.id}}\",\"state\":\"open\"}" }] },
{ "method": "POST", "path": "/orders", "order": "sequence",
"responses": [{ "status": 503 }, { "status": 201, "body": "{\"id\":\"{{uuid}}\"}" }] }
],
"outage": { "up_ms": 20000, "down_ms": 2000, "fault": "unavailable" }
}
}
]
}emulator 对象本身就是一个文档,signallab emulate 也能读取它。
在实验和脚本中
- 在实验中,模拟器 节点会在第一个步骤之前打开其模拟器,并一直应答到运行结束;它收到的内容会统计在报告中。其中的 HTTP 模拟器也是 等待 HTTP 请求(节点)所监听的对象,OSC 或 UDP 模拟器则与本次运行的等待节点共用端口。同一实验中同一传输协议的两个模拟器不能共用一个端口。参见节点和故障。
signallab emulate会从文件或此库运行模拟器,直到按下 Ctrl+C 或达到--for的时长,并输出它们的应答;参见命令行。