You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Node.js net/SSH2服务端如何实现写操作完成后再允许客户端输入

核心原理

问题本质是两个原因叠加导致:

  1. 客户端默认开启本地回显和行缓冲,按下的按键会直接在本地输出,同时提前攒在缓冲区批量发送,哪怕服务端还没处理完上一个请求
  2. 没有对输入加锁,服务端每收到一段数据就直接处理,不管上一次的写操作有没有完成

一、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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.30 14:09:03