基于Django+Xterm.js的浏览器终端多客户端隔离问题求助
问题
基于Django开发的浏览器终端应用,使用Xterm.js负责可视化与用户输入采集,终端逻辑由伪终端(pty)和bash进程处理。单客户端使用时运行正常,但新客户端连接或在新标签页打开时,会共用同一终端实例。由于客户端数量不确定,无法提前定义多个Xterm.js终端实例(如termA、termB)。尝试在Django中实现多进程未成功,寻求可行解决方案。
核心原因
当前代码使用**全局变量fd和child_pid**存储终端实例信息,所有客户端连接都会复用这两个变量,导致共用同一个bash进程。
解决方案
为每个Socket连接(对应唯一的sid)创建独立的pty与bash进程,使用字典存储每个sid对应的进程信息,实现客户端与终端实例的一一绑定。
修改步骤
- 替换全局变量为字典,存储每个客户端的进程信息
- 客户端连接时,为其创建独立的pty和bash进程
- 终端输出仅转发给对应的客户端
- 输入、窗口调整操作仅针对当前客户端的终端实例
- 客户端断开时,清理对应的进程与字典条目
修改后的代码
views.py
import os from django.shortcuts import render import socketio import pty import select import subprocess import struct import fcntl import termios import signal import eventlet async_mode = "eventlet" sio = socketio.Server(async_mode=async_mode) # 用字典存储每个客户端的会话信息:key为sid,value为{"pid": 进程ID, "fd": 文件描述符} client_sessions = {} def index(request): return render(request, "index.html") def set_winsize(fd, row, col, xpix=0, ypix=0): winsize = struct.pack("HHHH", row, col, xpix, ypix) fcntl.ioctl(fd, termios.TIOCSWINSZ, winsize) def read_and_forward_pty_output(sid): max_read_bytes = 1024 * 20 while True: sio.sleep(0.01) # 获取当前客户端的终端信息 session = client_sessions.get(sid) if not session: print(f"Session {sid} closed, stop forwarding") return fd = session["fd"] timeout_sec = 0 (data_ready, _, _) = select.select([fd], [], [], timeout_sec) if data_ready: output = os.read(fd, max_read_bytes).decode() # 仅向当前sid的客户端发送输出 sio.emit("pty_output", {"output": output}, room=sid) @sio.event def resize(sid, message): session = client_sessions.get(sid) if session: set_winsize(session["fd"], message["rows"], message["cols"]) @sio.event def pty_input(sid, message): session = client_sessions.get(sid) if session: os.write(session["fd"], message["input"].encode()) @sio.event def disconnect_request(sid): sio.disconnect(sid) @sio.event def connect(sid, environ): # 为每个新连接创建独立的pty和bash进程 (child_pid, fd) = pty.fork() if child_pid == 0: subprocess.run("clear") subprocess.run('bash') else: # 存储当前客户端的进程信息 client_sessions[sid] = {"pid": child_pid, "fd": fd} # 启动针对当前客户端的输出转发任务 sio.start_background_task(target=read_and_forward_pty_output, sid=sid) @sio.event def disconnect(sid): session = client_sessions.pop(sid, None) if session: child_pid = session["pid"] try: os.kill(child_pid, signal.SIGKILL) os.waitpid(child_pid, 0) print(f"Client {sid} disconnected, terminal process killed") except OSError: # 进程可能已提前结束,忽略错误 pass
index.html
<html> <head> {% load static %} <link rel="stylesheet" href="https://unpkg.com/xterm@5.2.1/css/xterm.css" /> <script src="https://unpkg.com/xterm@5.2.1/lib/xterm.js"></script> <script src="https://unpkg.com/xterm-addon-fit@0.7.0/lib/xterm-addon-fit.js"></script> <script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/2.4.0/socket.io.js"></script> </head> <body> <div class="main" style="margin-left: 5%; position:sticky;"> <div style="background: white; padding-bottom: 5px; margin-top:1%;"> <span>Status: <span style="font-size: small; background-color: #d9ff00;" id="status">connecting...</span></span> <button id="button" type="button" onclick="myFunction()" style="padding:0.2%;">Connect</button> </div> <div style=" width: 98%; height:50%;" id="terminal"></div> </div> <script> var socket = io.connect({ transports: ["websocket", "polling"], autoConnect: false }); const status = document.getElementById("status") const button = document.getElementById("button") const fit = new FitAddon.FitAddon(); // Clear sessionStorage when the page is reloaded or closed window.addEventListener('beforeunload', () => { sessionStorage.clear(); }); var term = new Terminal({ cursorBlink: true, }); term.loadAddon(fit); term.open(document.getElementById('terminal')); fit.fit(); term.onKey(e => { socket.emit("pty_input", { "input": e.key }); }) socket.on("pty_output", function (output) { term.write(output["output"]); }) socket.on("connect", () => { status.innerHTML = '<span style="background-color: lightgreen;">connected</span>' resize() button.innerHTML = 'Disconnect' }) socket.on("disconnect", () => { status.innerHTML = '<span style="background-color: #ff8383;">disconnected</span>' button.innerHTML = 'Connect' }) function myFunction() { if (button.innerHTML == 'Connect') { console.log("conn req") socket.connect(); } else if (button.innerHTML == "Disconnect") { socket.emit("disconnect_request") } } function resize() { console.log("resized") fit.fit() socket.emit("resize", { "cols": term.cols, "rows": term.rows }) } window.onresize = resize window.onload = resize </script> </body> </html>
关键说明
- 使用
client_sessions字典绑定每个sid与对应的终端进程,确保客户端隔离 read_and_forward_pty_output函数增加sid参数,仅向该客户端发送终端输出- 断开连接时从字典中移除对应条目并销毁进程,避免资源泄漏
- 前端无需大幅修改,每个页面会创建独立Socket连接,自动绑定到专属终端实例
内容的提问来源于stack exchange,提问作者ivenoidea
相关产品推荐
相关产品推荐

