跳转到内容

live-server 实时分发架构

将终端运行画面实时中继到 Web 浏览器,需要在毫秒级低延迟与应对高密文本吞吐的渲染效率之间取得严格平衡。 console2svg 的 live-server 子命令不依赖任何外部 Web 服务器或 Node.js 运行时,而是直接基于 .NET 标准套接字(TcpListener)自研了一套轻量级的专用 HTTP/1.1 与 SSE(Server-Sent Events:在 HTTP 之上由服务端向客户端单向流式推送文本事件的 Web 标准)服务端。 它剔除了冗余的协议开销,仅凭一个独立的二进制文件即可提供流畅的浏览器画面预览。

默认情况下,live-server 在 127.0.0.1:38473 监听连接。 它向浏览器客户端提供以下 5 个高度精简的专用端点:

  • /: 返回前端查看器界面(将 HTML、CSS 与客户端 JavaScript 打包为一体的嵌入式资源)。
  • /events: text/event-stream 格式的 SSE 推送流,用于持续向客户端传输终端最新的画面渲染数据。
  • /snapshot.svg: 按需实时将当前终端缓冲区渲染为独立完整的 SVG 图像并返回。
  • /health: 用于容器编排或健康探测的轻量端点(返回 {"status":"ok"})。
  • /settings: 供用户切换视口适配模式与调试监控面板的配置页面。

选择 SSE 而非 WebSocket,是因为单向屏幕广播无需客户端向服务端回传数据帧,避开了双向套接字的帧封包开销。 此外,SSE 与标准 HTTP 代理和防火墙高度兼容,且利用浏览器原生的 EventSource API 即可天然支持自动重连与事件类型路由。

三层分层解耦降低传输负载与 DOM 重构

Section titled “三层分层解耦降低传输负载与 DOM 重构”

如果每当光标微调或单字符变动时,都将包含窗口外框、阴影和标题栏的完整 SVG 重新传输并替换,不仅会白白浪费带宽,还会引发浏览器频繁的 DOM 重构与重排开销。 为了消除这一瓶颈,console2svg 将渲染内容拆解为 三个独立的 SVG 视觉图层 进行分发:

  1. 背景层(background): 绘制终端窗口外部边距与客户端主视口底色矩形。只要终端行列尺寸不发生改变,该层保持静态。
  2. 窗口层(window): 绘制窗口边框、标题栏文字、投影以及控制按钮。仅在终端尺寸调整或窗口标题变更时触发更新。
  3. 文本层(text): 绘制终端内部文本字符、字体样式、光标高亮以及行背景色。随着终端命令输出进行高频刷新。
SSE 流传输示例
event: background
data: <svg xmlns="http://www.w3.org/2000/svg" width="800" height="500">...</svg>
event: window
data: <svg xmlns="http://www.w3.org/2000/svg" width="800" height="500">...</svg>
event: text
data: <svg xmlns="http://www.w3.org/2000/svg" width="800" height="500">...</svg>

在前端,查看器预备了 3 个相互叠放的容器节点(#background、#window-paint 和 #text),根据收到的 SSE 事件名称,仅针对性更新对应容器的 innerHTML。 在常规命令行操作与文本编辑中,只有最顶层的 text 层会被替换,静态的窗口装饰与外边框完全免于重复解析。

服务端渲染循环与 Settle Delay 稳定化

Section titled “服务端渲染循环与 Settle Delay 稳定化”

终端模拟器缓冲区(ScreenBuffer)中的内容被修改时,服务端并不会立即光栅化生成 SVG。 许多 CLI 应用程序会将一帧画面拆分为若干微小的 ANSI 转义字符片段分批写入,若立即触发渲染,容易捕捉到屏幕清除一半或光标处于移动中途的不完整残影。

服务端的渲染后台任务(RenderLiveFramesAsync)利用内部定时器(约 60fps 周期)监控画面版本号(screenVersion),并实施如下防抖与节流控制:

  • Settle Delay(稳定化等待): 检测到缓冲区变更后,渲染器会等待一段 25 毫秒(LiveFrameSettleDelay)的静默窗口,确认其间没有新字符写入后再执行渲染。将突发转义序列合为一个原子帧,杜绝画面撕裂与闪烁。
  • FPS 上限节流: 即便面对持续大量涌出的日志输出,渲染调度也会限制在最高帧率之内(由 --video-fps 决定,默认 30fps 即约 33 毫秒每帧),避免耗尽服务器 CPU 资源。
  • 静态图层缓存: 当终端的行列数与上一帧相同时,系统将跳过 background 层的 SVG 生成,直接复用初次握手时构建的缓存字符串。

在弱网环境或浏览器标签页被切至后台休眠时,服务端套接字缓冲区可能会堆积未发出的帧,导致推流延迟不断累积。 在实时监控场景中,若画面比现实终端滞后数十秒是不可接受的。

管理客户端连接的 LiveSseClient 具备 背压抑制机制(Backpressure Mitigation),通过待发帧覆盖合并来解决此问题。 如果前一帧的套接字网络写入尚未完成,而服务端又生成了新的一帧,系统会直接丢弃未发出的旧帧,用最新的 SVG 快照覆盖待发缓冲区。

在受限网络下,这一策略保证了客户端跳过陈旧过程帧,始终接收到最新的终端画面。 此外,每次套接字写入均设有 5 秒超时机制,若客户端无响应将强制断开连接并回收资源,防止服务端内存泄漏。 同时,服务端每隔 2 秒推送一次轻量的 heartbeat 事件(ping),防止状态化 NAT 防火墙掐断空闲长连接。

即便服务端实施了节流,在密集滚屏时每秒仍可能有数十次 text 事件到达浏览器。 若前端每收到一个事件就同步执行一次 innerHTML 赋值,主线程将因高频样式重算与 DOM 解析而卡死。

前端查看器采用 rAF 聚合(Burst Coalescing)消除此卡顿。 当收到 text 事件时,数据仅缓存在内存变量(pendingText)中,真正的 DOM 注入被推迟到浏览器的下一个 requestAnimationFrame 垂直同步回调中统一执行。 若在单个屏幕刷新周期内收到多个事件,中间帧会被自动跳过,确保在浏览器准备重绘时仅应用最新的一帧数据。

查看器内部还内置了实时的性能诊断监控面板:

  • 事件吞吐率(events/s): 以蓝线绘制每秒实际应用的帧数。
  • Jank 掉帧卡顿率(jank ms/s): 测量超出屏幕刷新周期(垂直同步)1.5 倍的渲染延迟,以红线绘制。在完全流畅的渲染状态下,该数值保持为 0。
  • LoAF(长动画帧)观测: 利用浏览器的 Long Animation Frames API 捕获占用主线程超过 50 毫秒的长任务,并在日志中记录耗时与归属。

此外,前端提供了 4 种自适应缩放模式(保持纵横比最大化铺满的 contain、按宽度适配的 width、按高度适配的 height 以及等比像素 actual),确保在任何屏幕分辨率下文字与边框均锐利可读。