
本文介绍如何基于 django 后端与 xterm.js 前端,实现多用户并发访问时的独立终端会话——避免多个客户端共享同一伪终端(pty),确保每个连接拥有专属 bash 实例,并支持动态创建、销毁与资源回收。
本文介绍如何基于 django 后端与 xterm.js 前端,实现多用户并发访问时的独立终端会话——避免多个客户端共享同一伪终端(pty),确保每个连接拥有专属 bash 实例,并支持动态创建、销毁与资源回收。
在当前实现中,fd 和 child_pid 是全局变量,导致所有客户端强制复用同一个 PTY 进程——新连接会覆盖旧会话,甚至同用户多标签页也互相干扰。根本问题在于缺乏会话隔离机制:没有为每个 Socket.IO 客户端分配独立的 PTY 实例,也没有维护会话生命周期映射关系。
✅ 正确方案:按会话隔离 PTY 实例
核心思路是将 PTY 封装为可实例化的类(如 PtyTerminal),并为每个客户端连接动态创建、绑定、销毁专属终端实例。以下是关键改造步骤:
1. 封装 PTY 管理为独立类(推荐使用优化版)
# terminal/pty_manager.py
import os
import select
import pty
import struct
import termios
import signal
from threading import Lock
class PtyTerminal:
def __init__(self, cmd=None):
self.master = None
self.child = None
self._lock = Lock()
self.cmd = cmd or ["/bin/bash"]
# Fork PTY
child_pid, master_fd = pty.fork()
if child_pid == 0: # Child process
os.execv(self.cmd[0], self.cmd)
else: # Parent process
self.child = child_pid
self.master = master_fd
self._set_size(80, 24)
def _set_size(self, cols, rows):
if self.master:
winsize = struct.pack("HHHH", rows, cols, 0, 0)
try:
fcntl.ioctl(self.master, termios.TIOCSWINSZ, winsize)
except OSError:
pass
def resize(self, cols, rows):
with self._lock:
self._set_size(cols, rows)
def write(self, data):
if self.master and data:
try:
os.write(self.master, data.encode() if isinstance(data, str) else data)
except OSError:
pass
def read(self, max_bytes=1024):
if not self.master:
return b""
with self._lock:
ready, _, _ = select.select([self.master], [], [], 0.01)
if ready:
try:
return os.read(self.master, max_bytes)
except OSError:
return b""
return b""
def close(self):
with self._lock:
if self.master:
os.close(self.master)
self.master = None
if self.child:
try:
os.kill(self.child, signal.SIGTERM)
os.waitpid(self.child, 0)
except (OSError, ChildProcessError):
pass
self.child = None
✅ 优势:线程安全(Lock)、自动资源清理、可复用、支持自定义启动命令(如 ["/bin/bash", "-i"])。
Json Schema Toolkit下载使用 JSON Schema 验证 JSON 数据,从示例 JSON 生成 schema,并将其转换为 TypeScript 接口、Python 数据类或 Markdown 文档。
2. 按 Socket.IO sid 维护会话级终端映射
修改 views.py,移除全局 fd/child_pid,改用字典管理活跃会话:
# views.py(关键片段)
from django.shortcuts import render
import socketio
import eventlet
from .terminal.pty_manager import PtyTerminal
async_mode = "eventlet"
sio = socketio.Server(async_mode=async_mode)
# {sid: PtyTerminal} —— 每个连接独享终端实例
active_terminals = {}
def index(request):
return render(request, "index.html")
@sio.event
def connect(sid, environ):
# 创建专属终端
terminal = PtyTerminal(cmd=["/bin/bash", "-i"])
active_terminals[sid] = terminal
print(f"[+] New terminal for sid={sid}")
# 启动后台读取任务(每个会话独立)
sio.start_background_task(
target=read_pty_output,
sid=sid,
terminal=terminal
)
def read_pty_output(sid, terminal):
while sid in active_terminals:
output = terminal.read()
if output:
sio.emit("pty_output", {"output": output.decode('utf-8', errors='ignore')}, room=sid)
eventlet.sleep(0.02) # 防止忙等
@sio.event
def pty_input(sid, message):
if sid in active_terminals:
active_terminals[sid].write(message["input"])
@sio.event
def resize(sid, message):
if sid in active_terminals:
active_terminals[sid].resize(message["cols"], message["rows"])
@sio.event
def disconnect(sid):
if sid in active_terminals:
active_terminals[sid].close()
del active_terminals[sid]
print(f"[-] Terminal closed for sid={sid}")
3. 前端适配:确保消息定向广播
xterm.js 无需修改逻辑,但需确保 socket.emit() 的事件仅发给当前会话(Socket.IO 默认 room=sid 已满足)。注意前端 socket.on("pty_output") 无需改动——服务端已通过 room=sid 精准投递。
// index.html 中保持不变,但确保:
socket.on("pty_output", function (data) {
term.write(data.output); // ✅ 只收本会话输出
});
4. 补充健壮性措施(生产必备)
- 超时自动清理:定期扫描 active_terminals,对 5 分钟无活动的会话调用 .close()。
- 内存/进程数限制:在 connect 中检查 len(active_terminals),超限时拒绝新连接并返回 HTTP 503。
- 异常兜底:PtyTerminal.__init__ 中捕获 OSError(如 fork 失败),记录日志并抛出 ConnectionRefusedError。
- 信号隔离:避免 SIGCHLD 全局处理干扰;各 PTY 子进程由各自 waitpid 独立回收。
? 常见误区规避
- ❌ 不要复用全局 fd —— 这是并发冲突根源;
- ❌ 不要跨会话共享 PtyTerminal 实例;
- ❌ 不要在 connect 中直接 subprocess.run() —— 它阻塞主线程且无法交互;
- ❌ 不要忽略 pty.fork() 后的 os.execv —— 必须用 exec* 替换子进程镜像,否则 bash 不会接管 PTY。
✅ 最终效果
- ✅ 每个浏览器标签页/设备/用户获得独立 bash 会话;
- ✅ 输入输出完全隔离,互不干扰;
- ✅ 断开连接后自动释放 PTY 资源(文件描述符 + 进程);
- ✅ 支持无限水平扩展(理论上受限于系统 ulimit -n 和内存);
- ✅ 代码清晰、可测试、易维护。
通过封装 + 会话映射 + 生命周期管理三步,即可在 Django + Socket.IO + xterm.js 架构中稳健支撑高并发终端服务。











