跳到正文

模拟器 ​

模拟器就是由 Signal Lab 扮演的、您的系统所对接的 API、设备或服务。它监听一个地址,并按规则应答:HTTP API 按路由应答,OSC、UDP 或 TCP 设备按“收到这个,就回复那个”应答,MQTT 代理则像任何代理一样工作,另外还有自己的规则。它可以变慢、出错,或时不时停机,以便测试依赖出现异常时您的系统会怎样。每次交互都会被计数、列出,并发送到检查器。

一个模拟器就是一个文档。模拟器 界面维护一个模拟器库;同一个文档可以作为 模拟器 节点在实验中运行,可以通过命令行 signallab emulate 运行,也可以通过 API 和 MCP 运行,在哪里应答方式都一样。

界面 ​

左侧是库(库):每个模拟器及其协议和地址,正在运行的模拟器带有一个跳动的圆点和请求计数。右侧是所选模拟器的设置和规则,其下方是它收到的内容(实时)。

创建模拟器 ​

  1. 按库顶部的某个按钮:

    按钮创建监听于自带一条可直接使用的规则
    + HTTP APIHTTP API127.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 上以保留消息的形式发布相同的载荷

    如果库中已有其他模拟器使用该端口,则改用下一个空闲端口。

  2. 填写 名称(最多 120 个字符)。

  3. 设置 监听于:IP:port。127.0.0.1 只应答本机;0.0.0.0 也应答网络中的其他机器。

  4. 修改规则(见下文),并在 备注 中写明它所代替的对象。

更改会自动保存。创建副本 会在下一个空闲端口上创建一个副本。删除 会再确认一次(删除?),如果模拟器正在运行就停止它,然后将其从库中移除。

规则按从前到后的顺序尝试;第一条匹配的规则进行应答。每条规则的标题栏显示一行摘要;点击它即可展开或折叠该规则。↑ 和 ↓ 按钮用于移动规则,× 用于移除规则。

运行 ​

  1. 选择模拟器,按 启动。在按钮恢复之前,其端口就已打开:如果端口已被占用,或模拟器存在问题,会在这一步被拒绝并给出原因。
  2. 将您的系统指向它。对于 HTTP API,复制 URL 会复制其地址(http://127.0.0.1:18080),每个路由也都有一个 复制 URL 按钮,用于复制该路由自己的地址(路径中含有 {{…}} 模板时除外)。
  3. 观察 已接收 逐渐填满。
  4. 按 停止,或在控制台的任务条中停止它的任务。

按钮旁边的状态显示 未运行、它在哪里应答,或者它已停机。

模拟器会一直按启动时的规则应答。如果在它运行时修改了它,会出现 重启:按下它,即可按当前的规则重新启动。在此之前,规则上的命中次数会被隐藏,因为它们属于旧的规则。

停机 会让正在运行的模拟器不可用,直到您按下 恢复: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

条件读取请求的某一部分(位置),并进行比较:

位置名称读取
标头标头名称,不区分大小写该标头的值;标头发送了多次时,各值以 , 连接。
查询查询参数解码后的值;重复出现时取第一个。
正文—整个正文,作为文本。
JSONJSON 路径,例如 $.user.idJSON 正文中的该字段。

比较方式有 等于、不等于、小于、不大于、大于、不小于、包含、匹配正则、为空 和 不为空。数字按数值比较,文本精确比较。不存在的标头、参数或字段视为空。无法进行的比较(例如文本与数字比较)不成立。

响应 ​

一个路由有 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 JSON200,{"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 s2000 ms 后返回 200,{"ok":true}
无应答故障 无应答
连接关闭故障 关闭连接
格式错误的 JSON200,{"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 不可用

停机计划从模拟器启动时开始,并不断重复:在线、停机、在线、停机……停机期间:

模拟器遇到的情况
HTTP503 不可用: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 MiB413。
同时存在的 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该客户端会错过它们;计为 未送达。

从响应创建模拟器 ​

要根据一个成功的响应创建模拟器:

  1. 在 HTTP 界面上,发送请求并得到响应——或在实验的 HTTP 节点上使用 立即发送。
  2. 按响应旁边的 ⧉ 据此模拟。模拟此响应 对话框会显示它将创建的路由。
  3. 在 添加到 中,选择您的某个 HTTP 模拟器,或选择 新模拟器。
  4. 按 添加路由。模拟器 界面会打开并定位到该模拟器。

该路由以响应的状态、标头和正文来应答该请求的方法和路径(不含查询字符串)。只属于那一次交互的标头(Content-Length、Date、Server、ETag 等)会被去掉,正文按原样发送,即使其中含有 {{。新模拟器只包含这一个路由。添加到现有模拟器时,该路由会放在最前面,以便在更宽泛的路由之前应答;正在运行的模拟器会在您按 重启 后采用它。

从实验创建时,用模板写的 URL 会变成模式:其基础部分({{api}})会被去掉,整段为一个模板的路径段(/orders/{{order_id}})会变成 :order_id,只有部分使用模板的路径段则会以 * 结束路径。

初始模拟器 ​

Signal Lab 第一次找不到模拟器库时,会写入五个模拟器,都在本机上。它们的名称和备注以当时的界面语言写成。

模拟器监听于作用
演示 API127.0.0.1:8080GET /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:7100PING → 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 后通过临时文件整体写入,因此写入失败时会保留上一个版本。如果文件无法读取,列表会显示错误以及路径、行和列,文件保持原样:修复它,然后按 重新加载文件。手动编辑文件后,也请按 重新加载文件。没有文件时,会重新写入初始模拟器。

json
{
  "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 的时长,并输出它们的应答;参见命令行。