Node.js net/SSH2服务端如何实现写操作完成后再允许客户端输入
核心原理
问题本质是两个原因叠加导致:
- 客户端默认开启本地回显和行缓冲,按下的按键会直接在本地输出,同时提前攒在缓冲区批量发送,哪怕服务端还没处理完上一个请求
- 没有对输入加锁,服务端每收到一段数据就直接处理,不管上一次的写操作有没有完成
一、Net模块修改实现
给每个连接加输入锁,利用socket.write的回调函数确认写操作完成后再解锁,同时可以发送telnet控制指令关闭客户端本地回显和行缓冲,避免本地提前输出内容:
const net = require('net'); const server = new net.Server(); // Telnet控制指令:关闭本地回显 + 关闭行缓冲 const TELNET_DISABLE_ECHO = Buffer.from([0xff, 0xfb, 0x01, 0xff, 0xfb, 0x03]); server.on('connection', async function (socket) { console.log("Client connected!"); // 连接维度的输入锁 let inputLocked = false; socket.setEncoding('utf8'); // 发送控制指令完成后输出初始提示符再解锁 socket.write(TELNET_DISABLE_ECHO, () => { socket.write("$ ", () => { inputLocked = false; }); }); socket.on('data', async function (data) { // 锁状态下直接忽略输入 if (inputLocked) return; // 收到输入立即加锁 inputLocked = true; let input = data.toString().replace(/(\r\n|\n|\r)/gm, ""); if (input == "echo") { // 写操作全部完成后释放锁 socket.write("echo command executed\r\n$ ", () => { inputLocked = false; }); } else { socket.write("invalid command\r\n$ ", () => { inputLocked = false; }); } }); }); server.listen(1337, function() { console.log("listening"); });
二、SSH2模块修改实现
逻辑和net模块一致,给每个ssh shell流加输入锁,等写回调触发后再解锁:
var fs = require('fs'); var ssh2 = require('ssh2'); new ssh2.Server({ hostKeys: [fs.readFileSync('ssh.key')] }, function (client) { console.log('Client connected!'); let username = null; client.on('authentication', function (ctx) { if (ctx.method !== 'password') return ctx.reject(['password']); username = ctx.username; // 可自行补充密码校验逻辑 if (ctx.password) { ctx.accept(); } else { ctx.reject(); } }).on('ready', function () { console.log('Client authenticated!'); client.on('session', function (accept, reject) { var session = accept(); session.once('shell', function (accept, reject, info) { var stream = accept(); // 输入锁 let inputLocked = false; // 输出初始提示符后解锁 stream.write("$ ", () => { inputLocked = false; }); stream.on('data', function (data) { if (inputLocked) return; inputLocked = true; // 去除输入的换行符 let input = data.toString().replace(/(\r\n|\n|\r)/gm, ""); var args = input.split(" "); let output = ''; let isError = false; switch (args[0]) { case "echo": args.shift(); output = args.join(" ") + "\r\n"; break; case "whoami": output = username + "\r\n"; break; case "exit": stream.exit(0); stream.end(); return; default: isError = true; output = args[0] + ": No such command!\r\n"; break; } // 输出完成后写提示符,全部写完再解锁 const writeFinish = () => { stream.write("$ ", () => { inputLocked = false; }); }; isError ? stream.stderr.write(output, writeFinish) : stream.write(output, writeFinish); }); }); }); }).on('end', function () { console.log('Client disconnected'); }); }).listen(1337, function () { console.log('Listening on port ' + this.address().port); });
补充说明
- 加锁后哪怕客户端攒了大量输入批量发送,服务端也只会在上一次请求处理完成后才处理下一个,不会出现格式混乱的问题
- 关闭本地回显后如果需要显示用户输入的内容,可以在服务端收到字符后手动回显,交互逻辑完全可控
- 必须等待写操作的回调触发后再释放锁,才能适配公网高延迟的场景
内容的提问来源于stack exchange,提问作者Voltic
相关产品推荐
相关产品推荐

