浏览器里跑一个真正的 PostgreSQL 终端,不是 SQL 编辑器,不是查询 API,而是一个完整的 psql 会话——带 tab 补全、带历史记录、带所有终端语义。听起来像是把大象塞进冰箱,但我们确实做了。结果比预想中难得多。
核心矛盾在于三个硬性约束:服务端躲在 NAT 后面,xterm.js 只认 WebSocket,以及我们绝不能自己模拟 psql。这三条红线把常见的解法全堵死了。不能用 SSH 隧道,因为浏览器没有原生 SSH;不能长轮询,因为终端交互对延迟极度敏感;更不能做个假终端,因为用户要的就是真 psql 的行为一致性。
![]()
所以架构必须从数据流而非组件的角度来理解。整个系统是一条单向管道:浏览器 → 控制平面 → Redis Streams → 服务端 → PTY → psql,再原路返回。关键设计是服务端主动发起一切连接。控制平面从不主动连服务端,而是发一个 HTTP POST 信号,让服务端自己打开反向 WebSocket。这是穿透 NAT 的唯一干净方式。
连接建立的过程分六步。第一步,浏览器连上控制平面,创建一个 session_id,此时没有服务端、没有数据库、没有 PTY,只有一个逻辑会话在等。第二步,控制平面给服务端发信号,推一个带 session_id 的 HTTP 请求。第三步,服务端收到后主动连回控制平面,建立反向 WebSocket。第四步,控制平面把浏览器 handler 和服务端 handler 用 session_id 绑定,自己从传输层变成路由器。第五步,服务端调用 forkpty() 生成 PTY,子进程跑 psql,父进程管 I/O 线程。第六步,数据才真正流动起来。
最脆弱的边界在 PTY reader → Redis XADD → consumer → WebSocket 这一环。这是整个系统的稳定性模型。PTY 的 stdout 是阻塞的、字节流的、带 ANSI 转义序列的,而 WebSocket 是帧化的、可能掉线的、需要背压控制的。我们用 Redis Streams 做解耦,让 PTY reader 只管往流里写,consumer 异步拉取再推给浏览器。没有这层缓冲,WebSocket 的抖动会直接传导到 psql 进程,导致假死或数据截断。
生产环境还暴露了几个意料之外的难题。
第一,xterm.js 的输入事件是按键级粒度,但 psql 期望的是行缓冲模式,中间需要状态机做本地回显和行编辑。第二,PTY 的窗口大小变化(SIGWINCH)必须透传到远端,否则 psql 的排版会错乱。第三,也是最隐蔽的:某些数据库驱动在检测到非 TTY 时会自动切换输出格式,我们必须让 PTY 看起来足够像"真正的终端"来骗过它们。
这个系统的最终形态是反直觉的——控制平面几乎不持有状态,服务端是连接的发起方,Redis 成了事实上的消息总线。它不是为了优雅而设计,是为了在 NAT、浏览器沙箱和 Unix 进程语义的三重夹缝中存活下来。如果你也在做类似的事,记住一条:别把 WebSocket 直接怼到 PTY 上,中间必须有一层能吸收震动的缓冲。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.