Vue+xterm.js combined with websocket communication + zmodem protocol lrzsz upload and download

1. Instantiate xterm to generate a terminal terminal page

1. Install the related dependencies required by xterm

"xterm": "^4.9.0",
"xterm-addon-attach": "^0.6.0",  // 用来支持xterm链接websocket
"xterm-addon-fit": "^0.4.0",    // 用来让xterm终端自适应页面大小变化
"xterm-addon-unicode11": "^0.2.0",  // (非必要库)为xterm.js 提供 Unicode 版本 11 规则的插件

This is the dependency package required for terminal instantiation. The version can be adjusted by yourself. So far, I have not encountered any problems caused by the version.

2. Import after installing dependencies

import {
    
     Terminal } from "xterm";
import {
    
     FitAddon } from "xterm-addon-fit";
import {
    
     Unicode11Addon } from "xterm-addon-unicode11";
import _lodash from "lodash";

3. Write a dom to mount the terminal and introduce dependent packages

<template>
	<div id="term" style="background-color: #1e1e1e; height: 100%"></div>
</template>
<script>
import {
    
     Terminal } from "xterm";
import {
    
     FitAddon } from "xterm-addon-fit";
import {
    
     Unicode11Addon } from "xterm-addon-unicode11";
import "xterm/css/xterm.css";  // 这个css样式必须要引入,不然生成的terminal终端会有问题
</script>

4. Initialize the terminal

export default {
    
    
	data() {
    
    
		return{
    
    
			// 定义一些初始化变量
			term: null,
			unicode11Addon: new Unicode11Addon(),
	      	fitAddon: new FitAddon(),
		}
	},
	mounted() {
    
    
		this.initTerm()
	}
	methods: {
    
    
		// 初始化term终端
		initTerm() {
    
    
			// 实例化term以及依赖包
			this.term = new Terminal({
    
    
				cursorBlink: true,  // 光标闪烁
				cursorStyle: "underline",  // 光标闪烁样式
				rendererType: "canvas",  // 渲染类型
				theme: {
    
     background: "#1d242b", selection: "rgba(245, 108, 108, 0.5)" },  // 主题样式
				fontFamily: 'Consolas, Menlo, Monaco, "Courier New", monospace',
				scrollback: 10000,
			})
			this.term.loadAddon(this.fitAddon);
			this.term.loadAddon(this.unicode11Addon);
			this.term.unicode.activeVersion = "11";
			this.term.onResize((size) => {
    
     console.log(size.rows, size.cols) });
			// 将term挂载到dom元素上
			const terminalContainer = document.getElementById("term");
			this.term.open(terminalContainer);
			this.fitAddon.fit()
			this.term.focus()
			this.term.write("hello, welcome to terminal!")
		}
	}
}

Such a simple terminal page is rendered. If you still need to interact with websocket , you can continue to look down

Two, xterm and websocket communicate and perform data interaction

1. The websocket connection requires a url of the ws protocol as a parameter, which needs to be prepared according to your own business

2. Communication with the socket also requires the support of the zmodem.js library

If you don’t understand it, look at it in conjunction with the annotations, there are annotations for everything that should have annotations

<script>
import {
    
     Terminal } from "xterm";
import {
    
     FitAddon } from "xterm-addon-fit";
import {
    
     Unicode11Addon } from "xterm-addon-unicode11";
import "xterm/css/xterm.css"; 

// 安装这个库:npm install zmodem.js,因为这个库已经三年没更新了,所以也没有版本问题,直接安latest版本
// 因为这个库支持lrzsz命令,推荐使用
import Zmodem from "zmodem.js"; 

</script>
export default {
    
    
	data() {
    
    
		return{
    
    
			// 定义一些初始化变量
			term: null,
			unicode11Addon: new Unicode11Addon(),
	      	fitAddon: new FitAddon(),
	      	// 定义socket连接相关
	      	zsentry: Zmodem.Sentry,
	      	zsession: null,
	      	terminalSocket: null,
	      	termLoading: false,   // 我用来在连接时loading用,根据自己需求使用
		}
	},
	methods: {
    
    
		// 初始化term终端
		initTerm() {
    
    
			// 实例化term以及依赖包
			this.term = new Terminal({
    
    
				cursorBlink: true,  // 光标闪烁
				cursorStyle: "underline",  // 光标闪烁样式
				rendererType: "canvas",  // 渲染类型
				theme: {
    
     background: "#1d242b", selection: "rgba(245, 108, 108, 0.5)" },  // 主题样式
				fontFamily: 'Consolas, Menlo, Monaco, "Courier New", monospace',
				scrollback: 10000,
			})
			this.term.loadAddon(this.fitAddon);
			this.term.loadAddon(this.unicode11Addon);
			this.term.unicode.activeVersion = "11";
			this.term.onResize((size) => {
    
     console.log(size.rows, size.cols) });
			// 将term挂载到dom元素上
			const terminalContainer = document.getElementById("term");
			this.term.open(terminalContainer);
			this.fitAddon.fit()
			this.term.focus()
			this.term.write("hello, welcome to terminal!")
			<------------------websocket通信---------------------------->
			this.zsentry = new Zmodem.Sentry({
    
    
				// 这几个参数都为必要参数,是zmodem.js库源码示例中所必须的,每个参数都对应一个function
				// 为了代码可读,把相关方法单独拎了出来
				// 这几个内置函数在源码里都有,里面的逻辑有些小改动,可以对比查看
				to_terminal: this._to_terminal,  //发送的处理程序 到终端对象的流量。接收可迭代对象(例如,数组)包含八位字节数。
				sender: this._sender,  // 将流量发送到的处理程序对等方。例如,如果您的应用程序使用 WebSocket 进行通信到对等方,使用它将数据发送到 WebSocket 实例。
				on_detect: this._on_detect,  // 处理程序检测事件。接收新的检测对象。
				on_retract: this._on_retract,  // 于收回的处理程序事件。不接收任何输入。
			})
			this.initWsSocket();
			this.term.onData((data) => {
    
    
				this.terminalSocket.send(data);
			});
		},
		// 初始化连接socket
		initWsSocket() {
    
    
			if(!this.terminalSocket) {
    
    
				this.term.focus();
				// 实例化socket
				this.terminalSocket = new WebSocket("ws://10.22.33.444:5555/service/channel/123456");
				// socket断开事件监听
				this.terminalSocket.onclose = (e) => {
    
    
					this.term.write("连接已断开!");
				}
				this.terminalSocket.binaryType = "arraybuffer";
				// socket消息监听
				this.terminalSocket.onmessage = (e) => {
    
    
					try {
    
    
						this.zsentry.consume(e.data);
					} catch (error) {
    
    
						this.zsession._on_session_end();
						this.terminalSocket.send("\n");
					}
				}
			}
		},
		
		//  以下这些都是内置方法,和源码对比有一些小改动
		// 这个方法地址:https://github.com/FGasper/zmodemjs/blob/master/src/zmodem_browser.js
		send_files(session, files, options) {
    
    
	        if (!options) options = {
    
    };
	        //Populate the batch in reverse order to simplify sending
	        //the remaining files/bytes components.
	        var batch = [];
	        var total_size = 0;
	        for (var f=files.length - 1; f>=0; f--) {
    
    
	            var fobj = files[f];
	            total_size += fobj.size;
	            batch[f] = {
    
    
	                obj: fobj,
	                name: fobj.name,
	                size: fobj.size,
	                mtime: new Date(fobj.lastModified),
	                files_remaining: files.length - f,
	                bytes_remaining: total_size,
	            };
	        }
	        var file_idx = 0;
	        function promise_callback() {
    
    
	            var cur_b = batch[file_idx];
	            if (!cur_b) {
    
    
	                return Promise.resolve(); //batch done!
	            }
	            file_idx++;
	            return session.send_offer(cur_b).then( function after_send_offer(xfer) {
    
    
	                if (options.on_offer_response) {
    
    
	                    options.on_offer_response(cur_b.obj, xfer);
	                }
	                if (xfer === undefined) {
    
    
	                    return promise_callback();   //skipped
	                }
	                return new Promise( function(res) {
    
    
	                    var reader = new FileReader();
	                    //This really shouldn’t happen … so let’s
	                    //blow up if it does.
	                    reader.onerror = function reader_onerror(e) {
    
    
	                        console.error("file read error", e);
	                        throw("File read error: " + e);
	                    };
	                    var piece;
	                    reader.onprogress = function reader_onprogress(e) {
    
    
	                        //Some browsers (e.g., Chrome) give partial returns,
	                        //while others (e.g., Firefox) don’t.
	                        if (e.target.result) {
    
    
	                            piece = new Uint8Array(e.target.result, xfer.get_offset())
	                            // _check_aborted(session);
	                            if(session.aborted()) {
    
    
	                            	throw new Zmodem.Error("aborted")
	                            }
	                            xfer.send(piece);
	                            if (options.on_progress) {
    
    
	                                options.on_progress(cur_b.obj, xfer, piece);
	                            }
	                        }
	                    };
	                    reader.onload = function reader_onload(e) {
    
    
	                        piece = new Uint8Array(e.target.result, xfer, piece)
	                        // _check_aborted(session);
	                        if(session.aborted()) {
    
    
                            	throw new Zmodem.Error("aborted")
                            }
	                        xfer.end(piece).then( function() {
    
    
	                            if (options.on_progress && piece.length) {
    
    
	                                options.on_progress(cur_b.obj, xfer, piece);
	                            }
	                            if (options.on_file_complete) {
    
    
	                                options.on_file_complete(cur_b.obj, xfer);
	                            }
	                            //Resolve the current file-send promise with
	                            //another promise. That promise resolves immediately
	                            //if we’re done, or with another file-send promise
	                            //if there’s more to send.
	                            res( promise_callback() );
	                        } );
	                    };
	                    reader.readAsArrayBuffer(cur_b.obj);
	                } );
	            } );
	        }
	        return promise_callback();
	    },
	    on_detect(detection) {
    
    
		    //Do this if we determine that what looked like a ZMODEM session
		    //is actually not meant to be ZMODEM.
		    // if (no_good) {
    
    
		    //    detection.deny();
		    //    return;
		    //}
		    this.zsession = detection.confirm();
		    if (zsession.type === "send") {
    
    
		        //Send a group of files, e.g., from an <input>’s “.files”.
		        //There are events you can listen for here as well,
		        //e.g., to update a progress meter.
		        // Zmodem.Browser.send_files( zsession, files_obj );
		    }
		    else {
    
    
		        zsession.on("offer", (xfer) => {
    
    
		            //Do this if you don’t want the offered file.
		            //if (no_good) {
    
    
		            //    xfer.skip();
		            //    return;
		            //}
		            xfer.accept().then( () => {
    
    
		                //Now you need some mechanism to save the file.
		                //An example of how you can do this in a browser:
		                this._save_to_disk(
		                    xfer.get_payloads(),
		                    xfer.get_details().name
		                );
		            } );
		        });
		        zsession.start();
		    }
		},
		_save_to_disk(packets, name) {
    
    
	        var blob = new Blob(packets);
	        var url = URL.createObjectURL(blob);
	        var el = document.createElement("a");
	        el.style.display = "none";
	        el.href = url;
	        el.download = name;
	        document.body.appendChild(el);
	        //It seems like a security problem that this actually works;
	        //I’d think there would need to be some confirmation before
	        //a browser could save arbitrarily many bytes onto the disk.
	        //But, hey.
	        el.click();
	        document.body.removeChild(el);
	    },
	    _to_terminal(octets) {
    
    
	    	// i.e. send to the ZMODEM peer
	    	if(this.terminalSocket) {
    
    
	    		this.terminalSocket.send(new Uint8Array(octets).buffer);
	    	}
	    },
	    _on_retract() {
    
    
	    	//for when Sentry retracts a Detection
	    	console.log("retract")
	    }
	}
}

At this point, the basic xterm and websocket communication is completed. If you still need lrzsz command function based on the zmodem protocol , then look down

3. zmodem protocol lrzsz command upload and download function

1. The element-ui components used here

<template>
	<---rz命令上传文件--->
	<el-dialog
		title="请选择要上传的文件"
		:visible.sync="uploadDialogVisible"
		width="400px"
		:before-close="handleCloseUpload"
	>
		<el-upload
			ref="upload"
			action="http://localhost/posts/"
			multiple
			:auto-upload="false"
			v-loading="uploadLoading"
		>
			<el-button slot="trigger" type="primary">选取文件</el-button>
			<el-button type="primary" style="margin-left: 10px" @click="upload">上传</el-button>
		</el-upload>
	</el-dialog>
	<---sz命令下载文件--->
	<el-dialog
		title="正在下载请稍后"
		:visible.sync="downloadDialogVisible"
		width="400px"
		:before-close="handleCloseDownload"
	>
		<el-progress
			:percentage="percentage"
			:color="customColorMethod"
		></el-progress>
	</el-dialog>
</template>

2. Related variables and methods (the notes are marked)

If you don’t understand it, look at it in conjunction with the annotations, there are annotations for everything that should have annotations

export default {
    
    
	data() {
    
    
		return{
    
    
			// 定义一些初始化变量
			term: null,
			unicode11Addon: new Unicode11Addon(),
	      	fitAddon: new FitAddon(),
	      	// 定义socket连接相关
	      	zsentry: Zmodem.Sentry,
	      	zsession: null,
	      	terminalSocket: null,
	      	termLoading: false,   // 我用来在连接时loading用,根据自己需求使用
	      	// 上传下载需要的变量
	      	uploadLoading: false,
	      	uploadDialogVisible: false,
	      	downloadDialogVisible: false,
	      	percentage: 0,
		}
	},
	methods: {
    
    
		// rz命令上传文件
		upload() {
    
    
			let fileElem = document.getElementsByName("file")[0];
			if(fileElem.files.length > 0) {
    
    
				this.uploadLoading = true;
				const _t = this;
				// 这里就需要用到_send_files函数,函数在下面,里面的逻辑不用动
				// 这个内置函数需要传三个参数,具体参数介绍在git里面有,不做赘述
				// 第三个参数是一个object,包含三个回调函数,可以自己拎出来
				_t._send_files(this.zsession, fileElem.files, {
    
    
					// 上传响应
					on_offer_response(obj, xfer) {
    
    
						// 如果回调参数xfer为undefined,说明上传有问题
						if(xfer) {
    
    
							console.log(xfer)
						} else {
    
    
							_t.$notify.error({
    
    
								title: "Error",
								message: `${
      
      obj.name} was upload skipped`,
							});
						}
					},
					// 上传进度回调
					on_progress(obj, xfer) {
    
    
						let detail = xfer.get_details();
						let name = detail.name;
						let total = detail.size;
						let percent;
						if(total === 0) {
    
    
							percent = 100;
						} else {
    
    
							percent = Math.round((xfer.file_offset/total) * 100);
						}
						console.log(`${
      
      percent}%`)
					},
					// 上传成功回调
					on_file_complete(obj) {
    
    
						_t.$notify.error({
    
    
							title: "成功",
							message: `${
      
      obj.name} 上传成功`,
							type: "success"
						});
					}
				}).then(() => {
    
    
					fileElem.value = "";
					this.zsession.close();
					this.uploadDialogVisible = false;
					this.uploadLoading = false;
					this.terminalSocket.send("\n");
					this.term.focus();
				});
			}else {
    
    
				this.$message({
    
    
					type: "error",
					message: "请选择文件",
				});
				this.uploadLoading = false;
			}
			let upload = this.$refs.upload;
			upload.clearFiles();
		},
		// 上传文件弹框关闭
		handleCloseUpload() {
    
    
			if(this.uploadLoading) {
    
    
				this.$message({
    
    
					type: "error",
					message: "上传中无法关闭",
				});
			}else {
    
    
				this.zsession.close().then(() => {
    
    
					let upload = this.$refs.upload;
					upload.clearFiles();
				})
			}
		},
		// rzsz上传下载需要在这个内置函数中做一些改动
		// 用来控制上传下载dialog弹框的显隐和upload方法的触发
		on_detect(detection) {
    
    
		    //Do this if we determine that what looked like a ZMODEM session
		    //is actually not meant to be ZMODEM.
		    this.zsession = detection.confirm();
		    // 这里是监听上传事件
		    if (zsession.type === "send") {
    
    
		        //Send a group of files, e.g., from an <input>’s “.files”.
		        //There are events you can listen for here as well,
		        //e.g., to update a progress meter.
		        // Zmodem.Browser.send_files( zsession, files_obj );
		        
		        // 打开上传dialog弹框
		        this.uploadDialogVisible = true;  
		    }
		    // 这里监听下载事件
		    else {
    
    
		        zsession.on("offer", (xfer) => {
    
    
		            //Do this if you don’t want the offered file.

					// 这里是做了一个进度的计算,可有可无
		            let total = xfer.get_details().bytes_remaining;
	                let length = 0;
	                this.downloadDialogVisible = true;
	                xfer.on("input", (octets) => {
    
    
	                	length += octets.length;
	                	this.percentage = Math.ceil((length * 100) / total);
	                });
	                
	                // 这里往下是功能区
		            xfer.accept().then( () => {
    
    
		                //Now you need some mechanism to save the file.
		                //An example of how you can do this in a browser:'

						// 这个下载函数也是内置源码有的,在下面有,可以直接用
	                	this._save_to_disk(
		                    xfer._spool,
		                    xfer.get_details().name
		                );
		            });
		        });
		        // 监听到下载完毕,关闭下载弹框
		        this.zsession.on("session_end", () => {
    
    
		        	this.percentage = 0;
		        	this.downloadDialogVisible = false;
		        });
		        this.zsession.start();
		    }
		},
		/***--------------------分割线--------------------***/
		// 初始化term终端
		initTerm() {
    
    
			// 实例化term以及依赖包
			this.term = new Terminal({
    
    
				cursorBlink: true,  // 光标闪烁
				cursorStyle: "underline",  // 光标闪烁样式
				rendererType: "canvas",  // 渲染类型
				theme: {
    
     background: "#1d242b", selection: "rgba(245, 108, 108, 0.5)" },  // 主题样式
				fontFamily: 'Consolas, Menlo, Monaco, "Courier New", monospace',
				scrollback: 10000,
			})
			this.term.loadAddon(this.fitAddon);
			this.term.loadAddon(this.unicode11Addon);
			this.term.unicode.activeVersion = "11";
			this.term.onResize((size) => {
    
     console.log(size.rows, size.cols) });
			// 将term挂载到dom元素上
			const terminalContainer = document.getElementById("term");
			this.term.open(terminalContainer);
			this.fitAddon.fit()
			this.term.focus()
			this.term.write("hello, welcome to terminal!")
			<------------------websocket通信---------------------------->
			this.zsentry = new Zmodem.Sentry({
    
    
				// 这几个参数都为必要参数,是zmodem.js库源码示例中所必须的,每个参数都对应一个function
				// 为了代码可读,把相关方法单独拎了出来
				// 这几个内置函数在源码里都有,里面的逻辑有些小改动,可以对比查看
				to_terminal: this._to_terminal,
				sender: this._sender, 
				on_detect: this._on_detect,
				on_retract: this._on_retract,
			})
			this.initWsSocket();
			this.term.onData((data) => {
    
    
				this.terminalSocket.send(data);
			});
		},
		// 初始化连接socket
		initWsSocket() {
    
    
			if(!this.terminalSocket) {
    
    
				this.term.focus();
				this.terminalSocket = new WebSocket("ws:/?10.22.33.444:5555/service/channel/123456");
				this.terminalSocket.onclose = (e) => {
    
    
					this.term.write("连接已断开!");
				}
				this.terminalSocket.binaryType = "arraybuffer";
				this.terminalSocket.onmessage = (e) => {
    
    
					try {
    
    
						this.zsentry.consume(e.data);
					} catch (error) {
    
    
						this.zsession._on_session_end();
						this.terminalSocket.send("\n");
					}
				}
			}
		},
		
		//  以下这些都是内置方法,和源码对比有一些小改动
		// 这个方法地址:https://github.com/FGasper/zmodemjs/blob/master/src/zmodem_browser.js
		send_files(session, files, options) {
    
    
	        if (!options) options = {
    
    };
	        //Populate the batch in reverse order to simplify sending
	        //the remaining files/bytes components.
	        var batch = [];
	        var total_size = 0;
	        for (var f=files.length - 1; f>=0; f--) {
    
    
	            var fobj = files[f];
	            total_size += fobj.size;
	            batch[f] = {
    
    
	                obj: fobj,
	                name: fobj.name,
	                size: fobj.size,
	                mtime: new Date(fobj.lastModified),
	                files_remaining: files.length - f,
	                bytes_remaining: total_size,
	            };
	        }
	        var file_idx = 0;
	        function promise_callback() {
    
    
	            var cur_b = batch[file_idx];
	            if (!cur_b) {
    
    
	                return Promise.resolve(); //batch done!
	            }
	            file_idx++;
	            return session.send_offer(cur_b).then( function after_send_offer(xfer) {
    
    
	                if (options.on_offer_response) {
    
    
	                    options.on_offer_response(cur_b.obj, xfer);
	                }
	                if (xfer === undefined) {
    
    
	                    return promise_callback();   //skipped
	                }
	                return new Promise( function(res) {
    
    
	                    var reader = new FileReader();
	                    //This really shouldn’t happen … so let’s
	                    //blow up if it does.
	                    reader.onerror = function reader_onerror(e) {
    
    
	                        console.error("file read error", e);
	                        throw("File read error: " + e);
	                    };
	                    var piece;
	                    reader.onprogress = function reader_onprogress(e) {
    
    
	                        //Some browsers (e.g., Chrome) give partial returns,
	                        //while others (e.g., Firefox) don’t.
	                        if (e.target.result) {
    
    
	                            piece = new Uint8Array(e.target.result, xfer.get_offset())
	                            // _check_aborted(session);
	                            if(session.aborted()) {
    
    
	                            	throw new Zmodem.Error("aborted")
	                            }
	                            xfer.send(piece);
	                            if (options.on_progress) {
    
    
	                                options.on_progress(cur_b.obj, xfer, piece);
	                            }
	                        }
	                    };
	                    reader.onload = function reader_onload(e) {
    
    
	                        piece = new Uint8Array(e.target.result, xfer, piece)
	                        // _check_aborted(session);
	                        if(session.aborted()) {
    
    
                            	throw new Zmodem.Error("aborted")
                            }
	                        xfer.end(piece).then( function() {
    
    
	                            if (options.on_progress && piece.length) {
    
    
	                                options.on_progress(cur_b.obj, xfer, piece);
	                            }
	                            if (options.on_file_complete) {
    
    
	                                options.on_file_complete(cur_b.obj, xfer);
	                            }
	                            //Resolve the current file-send promise with
	                            //another promise. That promise resolves immediately
	                            //if we’re done, or with another file-send promise
	                            //if there’s more to send.
	                            res( promise_callback() );
	                        } );
	                    };
	                    reader.readAsArrayBuffer(cur_b.obj);
	                } );
	            } );
	        }
	        return promise_callback();
	    },
	    
	    // 这个函数在sz命令下载文件的时候用的到,也是源码写好的,可以直接用
		_save_to_disk(packets, name) {
    
    
	        var blob = new Blob(packets);
	        var url = URL.createObjectURL(blob);
	        var el = document.createElement("a");
	        el.style.display = "none";
	        el.href = url;
	        el.download = name;
	        document.body.appendChild(el);
	        //It seems like a security problem that this actually works;
	        //I’d think there would need to be some confirmation before
	        //a browser could save arbitrarily many bytes onto the disk.
	        //But, hey.
	        el.click();
	        document.body.removeChild(el);
	    },
	    _to_terminal(octets) {
    
    
	    	// i.e. send to the ZMODEM peer
	    	if(this.terminalSocket) {
    
    
	    		this.terminalSocket.send(new Uint8Array(octets).buffer);
	    	}
	    },
	    _on_retract() {
    
    
	    	//for when Sentry retracts a Detection
	    	console.log("retract")
	    }
	}
}

At this point, all the processes are over. The code can be used directly except for the url of the instantiated socket. The logic inside is complete
. Due to permission restrictions, it cannot be copied. The code is line by line, so it may there will be some small problems

insert image description here

Guess you like

Origin blog.csdn.net/weixin_45717984/article/details/130004395