WebSocket 全双工通信
HTTP 升级版, 握手阶段也采用Http 协议, 握手时通过 HTTP 服务器
Request Headers {
...
Connection: keep-alive, Upgrade,
Upgrade: websocket //升级websocket服务
Sec-WebSocket-Key: hash 串表示的key
Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits
Sec-WebSocket-Version: 13
...
}
Response Headers {
...
HTTP/1.1 101 switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: hash 串表示的key
...
}
WebShell Terminal 最佳实践
前端环境 React 17 + sockjs-client + Xterm@4.18.0 + ahooks 全家桶
注意: 从 Xtermjs 5 开始 使用 canvas 画布渲染需要搭配 xterm-addon-fit 使用
Xterm 全家桶
- Xterm
- Xterm-addon-attach
- xterm-addon-fit
- xterm-addon-web-links
import { Terminal } from 'xterm';
import React, { useEffect, useState, useRef } from 'react';
import { FitAddon } from 'xterm-addon-fit';
import { webLinksAddon } from 'xterm-addon-web-links';
import PropTypes from 'prop-types';
import SockJs from 'sockjs-client';
import { useDebounceEffect } from 'ahooks';
import { notification, Layout, message} from 'ant-design'
import './index.less';
import 'xterm/css/xterm.css';
const { Header } = Layout;
const WebTerminal = props => {
const { shellType } = props;
const terminal = useRef();
const prefix = useRef();
const terminalWindow = useRef(null);
const dockerSocket = useRef();
useEffect(() => {
initTerminal();
/**
* 业务代码初始化
*/
return () => {
//资源回收
if (dockerSocket.current) dockerSocket.current = null;
terminal.current.dispose();
if (terminal.current) terminal.current = null;
}
}, []);
//使用ONKey - 更安全的监听键盘输入事件
useEffect(() => {
if(terminal && shellType == 'terminal') {
terminal.current.onKey(e => {
const {key, domEvent} = e;
const { altGraphKey, metaKey } = domEvent;
const printable = !(altGraphKey || metaKey);
if(printable) dockerSocket.current.send(JSON.stringify({'op':'stdin', 'Data': key}));
})
}
});
//使用 onData,直接捕获输入数据, 喜欢用JQ 等同于JQ的on('data', () => {})
/*
useDebounceEffect(() => {
if(terminal && shellType == 'terminal') {
terminal.current.onData(data => {
let text= data.replace(/[\u4e00-\u9fa5]+/g, '');
dockerSocket.current.send(JSON.stringify({'op':'stdin', 'Data': text}));
})
}
})
*/
const initTerminal = () => {
// new Fit Size 对象
const fitAddon = new FitAddon();
// 风格设置
terminal.current = new Termianl({
rendererType: 'canvas', //采用 canvas 画布
cursorBlink: true, //显示光标
cursorStyle: "block", //设置背景色
disableStdin: shellType == 'log', //设置是否可输入
convertEol:true, //LF 和 CRLF
theme: {
foreground: '#ffffff',
background: '#000000',
cursor: 'help',
lineHeight: 18
} //定制主题
});
terminal.current.open(document.getElementById('terminal-container'));
terminal.current.loadAddon(fitAddon); //注册屏幕自适应组件
terminal.current.loadAddon(new WebLinksAddon());
//屏幕自适应
fitAddon.fit();
//监听浏览器窗口进行全屏自适应
window.addEventListener('resize', () => {
onTerminalResize();
fitAddon.fit();
});
if (!terminal.current._initialized) terminal.current._initialized = true;
terminal.current.prompt = () => terminal.current.write(`\r\n${prefix.current}`);
terminal.current.writeln(`Entering the container ... ... `);
terminal.current.writeln('');
//自定义处理按键
//基于https
terminal.current.attachCustomKeyEventHandler(event => {
const { code, type, ctrlkey } = event;
if(!navigator.clipboard) {
message.warning('当前为非安全网络,请升级https');
if (code === "KeyV" && type === 'keydown' && ctrlkey) {
return false;
}
return true;
} else {
if (code === 'keyC' && type === 'keydown' && ctrlkey) {
navigator.clipboard.writeText(terminal.current.getSelection());
return false;
}
if(code === 'keyV' && type === 'keydown' && ctrlKey) {
navigator.clipboard.readText().then(text => dockerSocket.current.send(JSON.stringify({ 'Op': 'stdin', 'Data': text })));
return false;
}
return false;
}
return true;
});
/*基于http*/
/* terminal.current.attachCustomKeyEventHandler(event => {
const { code, type, ctrlkey } = event;
if(code === 'keyV' && type === 'keydown' && ctrlKey) {
return false;
}
return true;
});
*/
if(shellType == 'termianl') {
getWebSocketSessionId().then(() => {
terminal.current.writeln('\x1b[1;1;36m 终端连接成功的话 。。。\x1b[0m');
}).catch(() => {
terminal.current.writeln('\x1b[1;1;31m 终端连接失败的话 。。。\x1b[0m');
});
}
}
const getWebSocketSessionId = () => new Promise((resolve, reject) => {
(//....后端请求接口).response(res => {
if(res.data) {
const { sessionId } = res.data;
dockerSocket.current = new SockJs(ws服务(hosturl, sessionId));
dockerSocket.current.onopen = onConnectOpen.bind(this, res.data);
dockerSocket.current.onmessage = onConnectionMessage.bind(this);
dockerSocket.current.onclose = onConnectionClose.bind(this);
dockerSocket.current.onerror = onConnectionError.bind(this);
resolve();
} else {
reject();
}
}, err => {
reject();
}, ...);
})
const onConnectOpen = (data) => {
dockerSocket.current.send(JSON.stringify({ 'Op': 'stdin', 'Data': data })));
onTerminalResize();
terminal.current.focus();
}
const onTerminalResize = () => {
const fullWidth = terminalWindow.current?.nativeElement?.parentElement?.clientWidth??默认值;
const fullHeight = terminalWindow.current?.nativeElement?.parentElement?.clientHeight??默认值;
let width = terminalWindow.current?.offsetParent?.clientWidth??fullWidth;
let height = terminalWindow.current?.offsetParent?.clientHeight??fullHeight;
let cols = (width - terminal.current?._core?.viewport?.scrollBarWidth -15) / terminal.current?._core?._renderService?.dimensions?.actualCellWidth;
let rows = height / terminal.current?._core?._renderService?.dimensions?.actualCellHeight;
let data = {
Cols: cols && cols != 'NaN' ? parseInt(cols) : 默认值;
Rows: rows && rows != 'NaN' ? parseInt(rows) : 默认值;
}
dockerSocket.current.send(JSON.stringify({ 'Op': 'resize', 'Cols': data.Cols, 'Rows': data.Rows })));
}
const onConnectionMessage = (event) => {
try{
const msg = JSON.parse(event.data);
prefix.current = msg.Data;
switch(msg['Op']) {
case 'stdout':
if(~msg['Data'].indexOf('\r')) {
onTerminalResize();
}
terminal.current.write(msg['Data']);
break;
case 'stdin':
case 'resize':
default: break;
}
}catch(e){
//错误通知
}
}
const onConnectionError = () => {
}
const onConnectionClose = (event) => {
}
return (
<div class="terminal">
<Layout>
<Header>
<div id="terminal-container" classNmae='c-webTerminal' ref={terminalWindow}/>
</Header>
</Layout>
</div>
)
}
WebTerminal.defaultProps = {
}
WebTerminal.propTypes = {
}
export default WebTerminal;