🔥🔥🔥websocket 前后端通信,接受命令,并执行

最近有一个需求,大概意思是,前后端websocket通讯,前端发终端执行命令出去,后端通过wsonmessage接收,接收到命令行,然后在后端的服务器上执行这条命令。

不知道我说明白了没,我画个图给大家瞧一瞧,这个画图工具挺有想法的,正好拿来用一用:

前端要做的工作:

🔥、把命令转换成buffer字节流通过ws传过去给后端

后端其中要做的工作:

🔥、后端接收到,转换字节流,然后执行命令行

整体:就是送快递📦,后端拆快递并用一下快递里面的物品的过程。

前端部分 -> 发命令

前端通过一个按钮点击发送ws消息过去给后端:

js 复制代码
// 添加测试按钮 - 执行PC端
const testButton = new ToolBoxTextButton('PC Test', '测试');
testButton.addEventListener('click', () => {
    // 将所有命令合并为一个,确保顺序执行
    const event = CommandControlMessage.createExecutePCCommand(
        `pkill -f gnirehtet || true && sleep 5 && lsof -ti:31416 | xargs kill -9 2>/dev/null || true && cd $HOME/gnirehtet-rust-linux64 && ./gnirehtet run ${udid}`,
    );
    client.sendMessage(event);
});
elements.push(testButton);

创按钮,这个按钮做什么呢?就是执行一条命令,这个命令可以不用细细探究(就是要执行run一个程序,因为开启一个端口去跑它的时候,第二次点击每次报错,就一开始先kill掉,再去跑)这么个东西。

然后发送到后端的websocket去。

js 复制代码
export class CommandControlMessage {
    // ...
    public static createExecutePCCommand(command: string): CommandControlMessage {
        const event = new CommandControlMessage(ControlMessage.TYPE_EXECUTE_PC_COMMAND);
        const commandBytes = Util.stringToUtf8ByteArray(command);
        const commandLength = commandBytes.length;

        let offset = 0;
        const buffer = Buffer.alloc(1 + 4 + commandLength);

        // 写入消息类型
        offset = buffer.writeInt8(event.type, offset);

        // 写入命令长度和内容
        offset = buffer.writeInt32BE(commandLength, offset);
        commandBytes.forEach((byte: number, index: number) => {
            buffer.writeUInt8(byte, index + offset);
        });

        event.buffer = buffer;
        return event;
    }
    // ...
}

"这段代码啊,就是专门用来打包命令的。打个比方,就像你要寄快递,得把东西装进箱子里贴好面单一样。

它主要做三件事:

  1. 先准备个箱子(Buffer),大小刚好能装下要发的命令
  2. 往箱子里塞东西:
    • 先塞个类型标签(消息类型)offset = buffer.writeInt8(event.type, offset);
    • 再写上命令有多长(命令长度)offset = buffer.writeInt32BE(commandLength, offset);
    • 最后把命令内容一个个字节码放进去
  3. 封箱打包好,就可以发出去(通过WebSocket)了 commandBytes.forEach((byte: number, index: number) => { buffer.writeUInt8(byte, index + offset); });

为什么要这么麻烦呢?因为网络传输就像寄快递,得按规矩打包好,对面才能正确拆包理解。这个打包过程就是确保命令能原原本本传到后端去执行。"

输出为:


后端部分 -> 收命令 + 做执行命令

js 复制代码
// 检查是否是 PC 命令
if (this.serial && this.isPCCommand(event.data)) {
    // 确保数据是Buffer类型
    const buffer = Buffer.isBuffer(event.data) ? event.data : Buffer.from(event.data);
    this.handlePCCommand(buffer);
    return;
}
js 复制代码
private async handlePCCommand(data: Buffer): Promise<void> {
    try {
        let offset = 1; // 跳过消息类型

        // 读取命令长度
        const commandLength = data.readInt32BE(offset);
        offset += 4;

        // 读取命令内容
        const command = data.subarray(offset, offset + commandLength).toString('utf8');

        console.log(`Executing PC command: ${command}`);

        // 执行PC端命令
        await this.executePCCommand(command);
    } catch (error) {
        console.error('Failed to handle PC command:', error);
    }
}

"这段代码是收命令的,收什么命令呢?就是前端发过来的那些要电脑执行的命令(比如之前说的先killrun那个)。"

"收到数据后先看看是不是我们要处理的命令,就像快递员先看是不是你家的快递。"

"确认是我们要的后,就把数据整理成标准格式,就像把快递包裹摆正。"

"拆数据包的时候很讲究:" "1. 先跳过开头没用的信息(就像撕掉快递单)" "2. 看看命令有多长(就像看看包裹大小)" -> const commandLength = data.readInt32BE(offset); "3. 把真正的命令内容读出来(就像拆开包裹拿东西)" -> const command = data.subarray(offset, offset + commandLength).toString('utf8');

"最后把这个命令交给电脑去执行,要是中途出错了就记下来。" -> await this.executePCCommand(command);

"整个过程就像收快递拆包裹一样,一步步来,确保收到的命令能正确执行。"

js 复制代码
private async executePCCommand(command: string): Promise<void> {
    try {
        const { spawn } = require('child_process');

        console.log(`Running PC command: ${command}`);

        return new Promise((resolve, reject) => {
            const process = spawn('sh', ['-c', command], {
                stdio: ['pipe', 'pipe', 'pipe'],
            });

            let stdout = '';
            let stderr = '';

            process.stdout.on('data', (data: Buffer) => {
                const output = data.toString();
                stdout += output;
                console.log('PC command stdout:', output.trim());
            });

            process.stderr.on('data', (data: Buffer) => {
                const output = data.toString();
                stderr += output;
                console.log('PC command stderr:', output.trim());
            });

            process.on('close', (code: number | null) => {
                if (code === 0) {
                    console.log('PC command executed successfully');
                    console.log('Final output:', stdout.trim());
                    resolve();
                } else {
                    console.error(`PC command failed with code ${code}`);
                    console.error('stderr:', stderr);
                    resolve(); // 继续执行,不中断流程
                }
            });

            process.on('error', (error: Error) => {
                console.error('Failed to start PC command process:', error);
                reject(error);
            });

            console.log('PC command running...');
        });
    } catch (error) {
        console.error('Failed to run PC command:', error);
        throw error;
    }
}

"这段代码就是用来执行电脑命令的,就像你点了个按钮让电脑干活。"

"具体怎么干呢?就是开个小黑窗(终端)来运行命令:"
const process = spawn('sh', ['-c', command])

"然后盯着这个小黑窗,看它说什么:"

"1. 正常输出就记下来(stdout)"
process.stdout.on('data')

"2. 报错信息也记下来(stderr)"
process.stderr.on('data')

"整个过程就像让个小弟去跑腿,你就在后面盯着他干活,干好干坏都记个账。"

"特别的是就算命令执行失败了(比如端口被占用),也不会卡住整个程序,而是继续往下走(resolve),就像小弟活没干好也先让他回来再说。"

在Node.js中,可以使用child_process模块来生成子进程。这个模块为我们提供了几种创建子进程的方法,包括spawn(),fork(),exec()和execFile()。这些方法可以帮助我们执行系统命令,运行其他语言的脚本,或者运行其他的Node.js文件。

  • 需要实时输出spawn()
  • spawn()方法用于异步地生成一个子进程,这个子进程可以运行系统命令、使用其他语言的脚本或者执行其他的应用。
  • 运行 Node 脚本并通信fork()
  • fork()方法是spawn()的一个特例,专门用于生成新的Node.js进程。fork()除了拥有spawn()的所有功能外,还增加了一个在父子进程之间通信的通道。
  • 执行简单命令exec()
  • exec()方法用于执行一个系统命令,并在命令完成后返回一个包含stdoutstderr的回调函数。这个方法比spawn()更简洁,但是对于大量数据或者需要实时处理数据的情况,建议使用spawn()
  • 运行可执行文件execFile()
  • execFile(方法类似于exec(),但是它是用于执行一个文件,而不是一个系统命令。与exec()相比,execFile()不会使用shell来执行命令,所以它可以更安全地执行文件名中包含空格、特殊字符等的文件。

验收效果 -> 收 + 执行

总结

送快递📦,后端拆快递并用一下快递里面的物品的这个需求,描述得贴切不贴切🐶。

相关推荐
JuneXcy12 分钟前
11.Layout-Pinia优化重复请求
前端·javascript·css
寻月隐君13 分钟前
Rust 泛型 Trait:关联类型与泛型参数的核心区别
后端·rust·github
泥泞开出花朵15 分钟前
LRU缓存淘汰算法的详细介绍与具体实现
java·数据结构·后端·算法·缓存
子洋21 分钟前
快速目录跳转工具 zoxide 使用指南
前端·后端·shell
天下无贼!22 分钟前
【自制组件库】从零到一实现属于自己的 Vue3 组件库!!!
前端·javascript·vue.js·ui·架构·scss
CF14年老兵43 分钟前
✅ Next.js 渲染速查表
前端·react.js·next.js
司宸1 小时前
学习笔记八 —— 虚拟DOM diff算法 fiber原理
前端
阳树阳树1 小时前
JSON.parse 与 JSON.stringify 可能引发的问题
前端
让辣条自由翱翔1 小时前
总结一下Vue的组件通信
前端
dyb1 小时前
开箱即用的Next.js SSR企业级开发模板
前端·react.js·next.js