概括
web终端可以在浏览器上模拟xshell的功能,本文源代码在Github: How to create a simple web terminal
效果如下所示:
架构
现在的web终端基本都是基于xterm.js开发的。xterm是一个前端的终端组件。
web终端的实现是客户端-服务器架构:
通信协议
- 客户端和服务器之间使用的是websocket协议
- 服务器通过本地fork一个bash或者powershell进程,充当传话筒角色
数据结构
服务器会维持一个,客户端的<socketId,pty> 的Map, 用来记录客户端连接和终端进程的对应关系。
客户端
客户端主要使用xterm和socketIO这两个库,xterm是终端组件,socketIO是和服务器通信的组件。
客户端显示仅仅需要一个DOM元素
<!DOCTYPE html>
<html>
<head>
<title>How to create web terminals</title>
<meta charset="UTF-8" />
<style>
#terminal-container{
position: absolute;
margin: 0;
padding: 0;
height: 100%;
width: 100%;
}
</style>
</head>
<body>
<div id="terminal-container"></div>
<script src="src/index.js"></script>
</body>
</html>
import { TerminalUI } from "./TerminalUI";
import io from "socket.io-client";
const serverAddress = "http://localhost:8080";
function connectToSocket(serverAddress) {
return new Promise(res => {
const socket = io(serverAddress);
res(socket);
});
}
function startTerminal(container, socket) {
const terminal = new TerminalUI(socket);
terminal.attachTo(container);
terminal.startListening();
}
function start() {
const container = document.getElementById("terminal-container");
connectToSocket(serverAddress).then(socket => {
startTerminal(container, socket);
});
}
这个文件主要设置了xterm实例如何发送客户端输入和接收服务端输出。
import { Terminal } from "xterm";
import { FitAddon } from 'xterm-addon-fit';
import "xterm/css/xterm.css";
export class TerminalUI {
constructor(socket) {
this.terminal = new Terminal({
theme: {
background: "black",
foreground: "#F5F8FA"
},
});
this.socket = socket;
}
startListening() {
this.terminal.onData(data =>{
this.sendInput(data)
});
this.socket.on("output", data => {
this.write(data);
});
}
write(text) {
this.terminal.write(text);
}
prompt() {
this.terminal.write(`\r\n$ `);
}
sendInput(input) {
this.socket.emit("input", input);
}
attachTo(container) {
let fitAddon = new FitAddon();
this.terminal.loadAddon(fitAddon);
this.terminal.open(container);
fitAddon.fit()
this.terminal.write("Terminal Connected");
this.terminal.write("");
this.prompt();
}
clear() {
this.terminal.clear();
}
}
服务端
服务端会给每个客户端连接socket生成一个终端进程,终端进程一般情况下linux是bash, windows是powershell.
会有一个全局的Map保存socket和终端进程的映射关系。
const socketIO = require("socket.io");
const PTYService = require("./PTYService");
class SocketService {
constructor() {
this.SocketBook=new Map()
}
attachServer(server) {
if (!server) {
throw new Error("Server not found...");
}
const io = socketIO(server);
console.log("Created socket server. Waiting for client connection.");
io.on("connection", socket => {
console.log("Client connect to socket.", socket.id);
socket.on("disconnect", () => {
console.log("Disconnected Socket: ", socket.id);
this.SocketBook.delete(socket.id)
});
let pty = new PTYService(socket);
this.SocketBook.set(socket.id,pty)
socket.on("input", input => {
let pty= this.SocketBook.get(socket.id)
pty.write(input);
});
});
}
}
module.exports = SocketService;
封装本地终端进程的相关操作,使用了node-pty库来fork终端进程。
const os = require("os");
const pty = require("node-pty");
class PTY {
constructor(socket) {
this.shell = os.platform() === "win32" ? "powershell.exe" : "bash";
this.ptyProcess = null;
this.socket = socket;
this.startPtyProcess();
}
startPtyProcess() {
this.ptyProcess = pty.spawn(this.shell, [], {
name: "xterm-color",
cwd: process.env.HOME,
env: process.env
});
this.ptyProcess.on("data", data => {
this.sendToClient(data);
});
}
write(data) {
this.ptyProcess.write(data);
}
sendToClient(data) {
this.socket.emit("output", data);
}
}
module.exports = PTY;
总结
不足:
- xterm的样式很难调节,本文示例中,我一直想要让输入字符输入达到DOM元素的长度再换行,但是没有做到。
- 如何写一个容器的web终端的话,如何将会话一开始的kubectl exec等信息清除。
成果:
- 实现了一个web终端的基础功能。
- 多客户端同时可以连接
|