ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

为什么 WebSocket Mock 不用 YAML、JSON 或 JS?我们自研了一套短 DSL

为什么 WebSocket Mock 不用 YAML、JSON 或 JS?我们自研了一套短 DSL 先说结论YAML、JSON、JS 都能做 WebSocket Mock而且能做很重的活。我们另写一套 DSL不是因为它们做不到而是轻量场景不该为这种通用性付费。HTTP 联调里Mock 很轻抓一帧勾特征改响应。一来一回就结束。WebSocket 的轻量场景其实也不长连上、登录、回一句、隔几秒推个心跳。要 Mock 的是一小段对话不是一篇程序。现成方案却常把这段对话塞进 JSON 规则表、YAML 配置或一段 JS。功能很强也能跑。问题是本来十来行能说完的事要先填字段、堆括号、写setInterval。轻量 Mock 先被容器写重了。我们在 DevPeek 里重新设计了一套 DSL图的就是这件事——让轻量 Mock 更轻。界面叫WS Flow文件扩展名是.dpws。人写对话工具链再把它编译成运行时需要的 JSON 和状态结构。HTTP 规则表为什么不能直接套过来最初我们也想复用 HTTP Mock给每条 WebSocket 消息做一条「消息条件 返回内容」规则。很快就发现两者看起来都在收发数据交互模型却不同。HTTP 是一次请求对应一次响应。规则命中后返回内容这次交互就结束了。WebSocket 则是一条连接里连续发生很多轮握手完成后服务端可能不等客户端发消息就先推challenge客户端登录成功后后续消息才进入「已登录」阶段一个请求可能先回 ACK再连续推送多帧结果heartbeat、行情等消息由定时器主动产生logout 或断线后之前启动的周期推送必须停止如果仍用平铺规则表就得额外增加onOpen、after、nextState、timerId、cancelWhen等字段。字段补得越多越接近把一个状态机拆散后塞进表格。所以 WS Flow 没有沿用 HTTP Mock 的形状。HTTP Mock 继续从抓包和 GUI 出发WebSocket Mock 直接描述「连接中的多轮消息树」。先定三个约束写语法之前我们先给它划了边界。第一读起来要像对话。条件写在上一层返回缩进在下一层下一轮消息继续往下缩进。即使没读过语言参考也应该大致看得出先后关系。第二服务端主动行为必须是一等公民。连接建立、定时推送、延迟发送和主动断开不能藏在插件或回调里否则最常见的长连接场景反而最难写。第三它不能长成另一门 JavaScript。不提供if、for、函数和任意表达式。复杂逻辑一旦需要真正的编程能力就交给 JSDSL 只把短会话写短。这三个约束也决定了语法的主体缩进负责结构少量符号负责匹配、生命周期和发送。同一段对话JSON 与 Flow 差在哪以「连上发 challenge → 登录成功 → 每 15 秒 tick」为例如果把它抽象成规则 JSON大致会写成这样{url:wss://api.example.com/ws,onOpen:[{send:{type:challenge}}],handlers:[{match:{type:login},reply:{type:loginSuccess},then:{every:15s,send:{type:tick}}}]}与这段对话直接相关的业务词只有challenge、login、loginSuccess和tick。其余字符大多在解释配置结构onOpen、handlers、match、reply、then。YAML 能去掉括号但去不掉这层壳仍然要写键、列表和嵌套对象而且缩进同时承担「配置归属」和「对话顺序」两种含义。JS 最灵活可一旦加上事件监听、定时器和清理逻辑一段 Mock 很容易变成需要维护的小程序。同一段Flow 是这样# ws wss://api.example.com/ws # profile type # ping auto --open challenge --typelogin loginSuccess --loop 15s tick--open表示连接建立先向客户端发送challenge它下面的--typelogin等待下一帧登录消息命中后再返回loginSuccess。单词返回会按文件头编译成{type:loginSuccess}业务帧不必每次手写 JSON。延迟和周期推送直接写在行上300ms、--loop 15s不必另起控制流。这里省掉的不只是字符数更是概念切换。写的人不必先想「我要创建一个 handler再给它挂一个 timer」只需按实际对话顺序往下写。缩进不只是排版它就是会话状态只写固定回复还不够。真实联调里经常要表达「登录以后才允许订阅」「退出房间后停止房间消息」。为此Flow 用 scope 表示连接当前走到哪一段。~auth进入已登录 scope!~auth退出它# profile type # capture * --typelogin ~auth {type:loginSuccess,token:$token} --loop 5s {type:heartbeat,ts:$now} --typerefresh {type:profile,token:$token} --typelogout !~auth logoutSuccess --typelogout {type:error,reason:not_logged_in}客户端发来登录帧后字段会按# capture *保存为连接变量返回里可用$token引用。进入auth后5 秒一次的 heartbeat 开始运行收到 logout 时退出auth绑定在这个 scope 上的定时器也随之取消。最后一个普通--typelogout是未登录时的兜底。它和--typelogout !~auth写的是同一个业务动作但是否处于authscope 决定了走成功还是报错。这也是我们没有把--loop简单翻译成全局setInterval的原因。定时器必须属于某段会话进入时启动退出时回收否则 Mock 跑久了会不断留下幽灵推送。用 Profile 适配协议而不是复制一套语法并不是所有 WebSocket 协议都用type分发消息。有的用event、action、method还有的是纯文本。Flow 把这些差异放进文件头# profile event # dispatch event # format json # capture * --eventmessage {event:message,text:收到$text}在eventProfile 下单词返回welcome会编译为{event:welcome}换成默认的typeProfile则是{type:welcome}。JSON-RPC 可以按method匹配纯文本协议可以用--...匹配整帧。Profile 只提供默认约定不改变 Flow 的结构。这样不需要为每种业务协议发明新的关键字也不会把协议字段硬编码进解析器。少量符号各自只做一件事我们刻意控制了符号预算--fieldvalue匹配客户端消息嵌套条件表示 AND--open、--loop连接和定时等生命周期事件~name、!~name进入或退出命名 scope$name引用当前连接捕获到的变量file从文件读取较大的返回内容|对返回内容做模板、取值或字段修改500ms延迟发送!close发送后关闭连接同一条件下写多行返回会按顺序发送每行都能有自己的延迟。于是「立即 ACK300ms 后推一帧进度再过 400ms 推最终结果」不需要数组和调度代码# capture id --typeagent {type:ack,id:$id,ok:true} fixtures/agent-progress.json | template 300ms fixtures/agent-final.json | template 400ms语法看起来短但运行语义不能含糊。例如--typeping匹配的是 JSON 业务消息--ping匹配的是 WebSocket 协议级 Ping 帧两者不能混为一谈。自研 DSL 最贵的不是 Parser把缩进文本解析成 AST 并不算最难。真正贵的是让失败可理解、行为可验证。如果只做一个能跑的 Parser用户写错缩进、引用了不存在的变量或在 Mock 接管连接后忘记处理协议 Ping最后看到的只会是「怎么没消息」或「为什么一会儿就断线」。这类静默失败比多写几行 JSON 更糟。因此.dpws不是直接边读边执行而是经过 parse、validate、compile再交给 runtime。校验会给出稳定错误码和行号例如缩进结构非法未知的生命周期事件或管道$变量未定义scope 重复定义Mock 截断 upstream却没有启用# ping auto或处理--ping校验已经接进编辑器高亮和补全还在路上。我们还保留了 simulate 这一层让同一份 Flow 可以输入 open、message、tick、close 事件再检查输出帧、scope 变化和定时器是否按预期停止。轻的用 DSL重的仍用 JSFlow只对准轻量 Mock顺序的一小段会话。连接欢迎、登录、订阅、几次推送、心跳和退出都在它的舒适区。下面这些情况则应该停下来考虑 JS 或专门的 Mock 服务多连接之间需要共享并实时修改状态消息会并发、乱序分支依赖复杂时序payload 需要大量随机生成或动态计算要访问数据库、外部 API 或执行任意用户代码团队更看重 JSON Schema、既有 CI 和通用编辑器生态这不是 DSL 没写完而是边界有意为之。每增加一种表达能力都要同时增加解析、报错、调试和文档成本。若最终补齐变量赋值、条件表达式、函数、模块和异步 API我们只是重新造了一门更陌生的脚本语言。自研当然也有账Parser、错误提示、高亮、补全和版本兼容都要自己维护同事也要学习一组新符号。换来的价值必须足够具体——打开文件能直接读出对话顺序修改一条返回不用先理解一套配置 Schema周期推送能随会话 scope 自动启停。回到最初的问题我们不是因为 YAML、JSON 或 JS 做不到才设计 WS Flow。恰恰相反它们什么都能做问题在于轻量场景也要为这种通用性付费。.dpws选择的是另一种交换牺牲任意编程能力换取更短的业务表达增加一套受控语法换取可编译、可校验、可模拟的会话模型。判断它是否值得不看少写了多少括号而看联调的人能不能在几十秒内回答三个问题客户端发什么会命中服务端接着回什么这个阶段什么时候结束如果一份 Flow 打开后就能直接回答这套 DSL 才算达到了目标。相关链接官网原文为什么我们重新设计了 WebSocket Mock DSLWebSocket Mock 文档.dpws语言参考.dpwsScope.dpws文件头.dpws错误码同一条线上的另外两篇抓包历史从 sql.js 迁到原生 SQLite、桌面壳从 Electron 换成 Tauri若你也在联调长连接欢迎到 GitHub Discussions 聊聊。
返回列表