最近有一个需求,大概意思是,前后端websocket通讯,前端发
终端执行命令
出去,后端通过ws
的onmessage
接收,接收到命令行,然后在后端的服务器上执行这条命令。
不知道我说明白了没,我画个图给大家瞧一瞧,这个画图工具挺有想法的,正好拿来用一用:

前端要做的工作:
🔥、把命令转换成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;
}
// ...
}
"这段代码啊,就是专门用来打包命令的。打个比方,就像你要寄快递,得把东西装进箱子里贴好面单一样。
它主要做三件事:
- 先准备个箱子(Buffer),大小刚好能装下要发的命令
- 往箱子里塞东西:
- 先塞个类型标签(消息类型)
offset = buffer.writeInt8(event.type, offset);
- 再写上命令有多长(命令长度)
offset = buffer.writeInt32BE(commandLength, offset);
- 最后把命令内容一个个字节码放进去
- 先塞个类型标签(消息类型)
- 封箱打包好,就可以发出去(通过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);
}
}
"这段代码是收命令
的,收什么命令呢?就是前端发过来的那些要电脑执行的命令(比如之前说的先kill
再run
那个)。"
"收到数据后先看看是不是我们要处理的命令,就像快递员先看是不是你家的快递。"
"确认是我们要的后,就把数据整理成标准格式,就像把快递包裹摆正。"
"拆数据包的时候很讲究:" "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()
方法用于执行一个系统命令,并在命令完成后返回一个包含stdout
和stderr
的回调函数。这个方法比spawn()
更简洁,但是对于大量数据或者需要实时处理数据的情况,建议使用spawn()
。

- 运行可执行文件 →
execFile()
: execFile(
方法类似于exec()
,但是它是用于执行一个文件,而不是一个系统命令。与exec()
相比,execFile()
不会使用shell
来执行命令,所以它可以更安全地执行文件名中包含空格、特殊字符
等的文件。

验收效果 -> 收 + 执行

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