Java自学者论坛

 找回密码
 立即注册

手机号码,快捷登录

恭喜Java自学者论坛(https://www.javazxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,会员资料板块,购买链接:点击进入购买VIP会员

JAVA高级面试进阶训练营视频教程

Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程Go语言视频零基础入门到精通Java架构师3期(课件+源码)
Java开发全终端实战租房项目视频教程SpringBoot2.X入门到高级使用教程大数据培训第六期全套视频教程深度学习(CNN RNN GAN)算法原理Java亿级流量电商系统视频教程
互联网架构师视频教程年薪50万Spark2.0从入门到精通年薪50万!人工智能学习路线教程年薪50万大数据入门到精通学习路线年薪50万机器学习入门到精通教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程MySQL入门到精通教程
查看: 440|回复: 0

如何解决Vue.js里面noVNC的截图问题之后篇——用web虚拟终端作为替代功能

[复制链接]
  • TA的每日心情
    奋斗
    4 天前
  • 签到天数: 802 天

    [LV.10]以坛为家III

    2053

    主题

    2111

    帖子

    72万

    积分

    管理员

    Rank: 9Rank: 9Rank: 9

    积分
    726006
    发表于 2021-5-29 15:47:27 | 显示全部楼层 |阅读模式

      使用node.js开发webSocket开发代理,可以解决webSocket的cookies跨域问题。

      这时有人会说,如果openstack的虚拟桌面流量太大,把代理冲内存溢出了,如何处理?

      实际上,不是什么人都特别需要用WEB虚拟桌面操控虚拟机或物理机的,除非是windows系统,linux系统完全可以用流量更小的虚拟终端登陆。实际上进入linux虚拟桌面之后,好多操作不是还要用终端的吗?

      业务层面,推荐使用linux系虚拟桌面的用户使用终端,甚至完全不提供虚拟桌面,这才是解决流量拥塞的方法。

      然而想要在web上操纵linux终端,就需要通过 SSH 代理的方式调用并返回一个 shell 的虚拟终端(pty)的开源的 Web Terminal 项目。

      这里为了防止SSH代理与项目耦合,导致代码难以查找,用node.js中间件或者Java的Springboot实现。

      node.js的服务端实现(node.js对于websocket服务端的解决方法有二:原生websocket包和socket.io,后者可以在浏览器不支持的情况下转换为sockJS链接):

    var http = require('http');
    var io = require('socket.io');
    var utf8 = require('utf8');
    var SSHClient = require('ssh2').Client;
    
    var server2 = http.createServer(function(request, response) {
        console.log((new Date()) + ' Server is reseiveing on port 4041');
        response.writeHead(204);
        response.end();
    });
    server2.listen(4041, function() {
        console.log((new Date()) + ' Server is listening on port 4041');
    });
    io = io.listen(server2,{origins: '*:*'});
    function createNewServer(machineConfig, socket) {
        var ssh = new SSHClient();
        let {msgId, ip, username, password, port, rows, cols} = machineConfig;
        ssh.on('ready', function () {
            socket.emit(msgId, '\r\n***' + ip + ' SSH CONNECTION ESTABLISHED ***\r\n');
            ssh.shell(function(err, stream) {
                if(rows != null && cols != null)
                    stream.setWindow(rows, cols);
                if(err) {
                    return socket.emit(msgId, '\r\n*** SSH SHELL ERROR: ' + err.message + ' ***\r\n');
                }
                socket.on(msgId, function (data) {
                    var mydata = data.data;
                    if(mydata != null){
                        console.log(">>>" + data.data + "<<<");
                        stream.write(mydata);
                    }
                    var size = data.rows;
                    if(size != null){
                        stream.setWindow(data.rows, data.cols);
                    }
                });
                stream.on('data', function (d) {
                    try{
                        var mydata = utf8.decode(d.toString('binary'));
                        mydata = mydata.replace(/ \r(?!\n)/g,'');
                        console.log("<<<" + mydata + ">>>");
                        socket.emit(msgId, mydata);
                    }catch(err){
                        socket.emit(msgId, '\r\n*** SSH CONNECTION ERROR: ' + err.message + ' ***\r\n');
                    }
                }).on('close', function () {
                    ssh.end();
                });
            })
        }).on('close', function () {
            socket.emit(msgId, '\r\n*** SSH CONNECTION CLOSED ***\r\n');
            ssh.end();
        }).on('error', function (err) {
            console.log(err);
            socket.emit(msgId, '\r\n*** SSH CONNECTION ERROR: ' + err.message + ' ***\r\n');
            ssh.end();
        }).connect({
            host: ip,
            port: port,
            username: username,
            password: password
        });
    }
    
    io.on('connection', function(socket) {
        socket.on('createNewServer', function(machineConfig) {//新建一个ssh连接
            console.log("createNewServer");
            createNewServer(machineConfig, socket);
        })
    
        socket.on('disconnect', function(){
            console.log('user disconnected');
        });
    })
    node ssh代理

      Vue.js代码这样写(需导入xterm):

    <template>
    </template>
     
    <script>
    import 'xterm/dist/xterm.css'
    import { Terminal } from 'xterm';
    import * as fit from 'xterm/dist/addons/fit/fit';
    import * as fullscreen from 'xterm/dist/addons/fullscreen/fullscreen'
    import openSocket from 'socket.io-client';
        export default {
          name: 'sshweb',
          props:['ip','port'],
            data () {
                return {
                wsServer:null,
                localip:'',
                localport:'',
                env: "",
                podName: "",
                contaName: "",
                logtxt: "",
                term:[0,0],
                colsLen:9,
                rowsLen:19,
                colRemain:21,
                msgId:0,
                col:80,
                row:24,
                 terminal: {
                        pid: 1,
                        name: 'terminal',
                        cols: 80,
                        rows: 24
                      },
                    
                }
            },
            watch:{
                port(val){
                this.localport=port;
                },
                ip(val){
                this.localip=ip;
                }
            },
            methods: {
                S4() {
                    return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
                },
                guid() {
                    return (this.S4()+this.S4()+"-"+this.S4()+"-"+this.S4()+"-"+this.S4()+"-"+this.S4()+this.S4()+this.S4());
                },
                createServer1(){
                    this.msgId = this.guid();
                    var msgId = this.msgId;
                    var myserver = this.wsServer;
                    var selfy = this;
                    var ipport = this.$route.params.ipport.split(':');
                    var myport = ipport[0];
                    var myip = ipport[1];
                    myserver.emit("createNewServer", {msgId: msgId, ip: myport, username: "root", password: "xunfang", port: myip, rows: this.term[0].rows, cols: this.term[0].cols});
                    let term = this.term[0];
                    term.on("data", function(data) {
                        myserver.emit(msgId, {'data':data});
                    });
                    
                    myserver.on(msgId, function (data) {
                        term.write(data);
                    });
                    term.attachCustomKeyEventHandler(function(ev) {
                        if (ev.keyCode == 86 && ev.ctrlKey) {
                            myserver.emit(msgId, new TextEncoder().encode("\x00" + this.copy));
                        }
                    });
                    myserver.on('connect_error', function(data){
                        console.log(data + ' - connect_error');
                    });
                    myserver.on('connect_timeout', function(data){
        console.log(data + ' - connect_timeout');
    });
    myserver.on('error', function(data){
        console.log(data + ' - error');
    });
    myserver.on('disconnect', function(data){
        console.log(data + ' - disconnect');
    });
    myserver.on('reconnect', function(data){
        console.log(data + ' - reconnect');
    });
    myserver.on('reconnect_attempt', function(data){
        console.log(data + ' - reconnect_attempt');
    });
    myserver.on('reconnecting', function(data){
        console.log(data + ' - reconnecting');
    });
    myserver.on('reconnect_error', function(data){
        console.log(data + ' - reconnect_error');
    });
    myserver.on('reconnect_failed', function(data){
        console.log(data + ' - reconnect_failed');
    });
    myserver.on('ping', function(data){
        console.log(data + ' - ping');
    });
    myserver.on('pong', function(data){
        console.log(data + ' - pong');
    });
                },
                resize(row,col){
                    row = Math.floor(row/this.rowsLen);
                    col = Math.floor((col-this.colRemain)/this.colsLen);
                    if(row<24)row=24;
                    if(col<80)col=80;
                    if(this.row != row || this.col != col){
                        this.row=row;
                        this.col=col;
                        this.term[0].fit();
                        //this.term[0].resize(col,row);
                        this.wsServer.emit(this.msgId, {'rows':this.term[0].rows.toString(),'cols':this.term[0].cols.toString()});
                        //this.wsServer.emit(this.msgId, {'rows':row.toString(),'cols':col.toString()});
                    }
                }
            },
            mounted(){
                this.wsServer = new openSocket('ws://127.0.0.1:4041');
                var selfy = this;
                window.onload = function(){
                    for(var i = 0;i < 1;i++){
                        var idname = 'net0';
                        Terminal.applyAddon(fit);
                        Terminal.applyAddon(fullscreen);
                        var terminalContainer = document.getElementById('app');
                        //terminalContainer.style.height = (selfy.rowsLen * selfy.terminal.rows).toString() + 'px' ;
                        //terminalContainer.style.width = (selfy.colsLen * selfy.terminal.cols + selfy.colRemain).toString() + 'px' ;
                        selfy.term = new Terminal({
                            cursorBlink: true
                        });
                        selfy.term.open(terminalContainer, true);
                        if(window.innerWidth > 0 && window.innerHeight > 0)
                            selfy.term.fit();
                        selfy.createServer1();
                    }
                }
                $(window).resize(() =>{
                    if(window.innerWidth > 0 && window.innerHeight > 0){
                        selfy.resize(window.innerHeight, window.innerWidth);
                    }
                });
            },
            components: {
            }
        }
    </script>
     
    <style scoped>
    #app{height:100%;width:100%;}
    </style>
    Vue.js的ssh客户端

      打开服务器和Vue系统,登陆这个客户端上SSH,大功告成!

      Java部分:

      这里有个完整的解决方案,在我改正过的github仓库里。

      这里说一下websocket连接代码:

      vip.r0n9.ws.WebSshHandler:

    package vip.r0n9.ws;
    
    import com.jcraft.jsch.JSchException;
    import org.springframework.stereotype.Component;
    import vip.r0n9.util.SshSession;
    
    import javax.websocket.*;
    import javax.websocket.server.PathParam;
    import javax.websocket.server.ServerEndpoint;
    import java.io.*;
    import java.util.HashMap;
    import java.util.Map;
    import java.util.concurrent.ConcurrentHashMap;
    
    @ServerEndpoint(value = "/ssh/{id}", configurator = WebSocketConfigrator.class)
    @Component
    public class WebSshHandler {
    
        private static Map<String, SshSession> map = new ConcurrentHashMap<String, SshSession>();
    
        @OnOpen
        public void onOpen(final Session session, @PathParam("id") String id) throws JSchException, IOException, EncodeException, InterruptedException {
            System.out.println("有新链接 " + session.getUserProperties().get("ClientIP") + " 加入!当前在线人数为" + getOnlineCount());
    
            Map<String,String> parammap = new HashMap<String,String>();
            String[] param =  session.getQueryString().split("&");
            for(String keyvalue:param){
               String[] pair = keyvalue.split("=");
               if(pair.length==2){
                   parammap.put(pair[0], pair[1]);
               }
            }
            
            String hostname = parammap.get("hostname");
            String password = parammap.get("password");
            Integer port,cols,rows;
            try {
                port = Integer.valueOf(parammap.get("port"));
            }catch(Exception e) {
                port = 22;
            }
            String username = parammap.get("username");
            try {
                rows = Integer.valueOf(parammap.get("rows"));
            }catch(Exception e) {
                rows = 24;
            }
            try {
                cols = Integer.valueOf(parammap.get("cols"));
            }catch(Exception e) {
                cols = 80;
            }
            
            SshSession sshSession;
            sshSession = new SshSession(hostname, port, username, password, session, rows, cols);
            map.put(session.getId(), sshSession);
        }
    
        @OnClose
        public void onClose(Session session) {
            SshSession sshsession = map.remove(session.getId());
            sshsession.close();
        }
    
        @OnMessage
        public void onMessage(String message, Session session) throws IOException, JSchException {
            map.get(session.getId()).getMessage(message);
        }
        
        @OnError
        public void onError(Session session, Throwable throwable) {
            throwable.printStackTrace();
            try {
                session.getBasicRemote().sendText(throwable.getMessage());
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        public static synchronized int getOnlineCount() {
            return map.size();
        }
    }
    WebSshController.java

      这里就是websocket服务端代码,连接websocket首先要在这里进行处理。

      vip.r0n9.util.SshSession:

    package vip.r0n9.util;
    
    import java.io.BufferedReader;
    import java.io.IOException;
    import java.io.InputStream;
    import java.io.InputStreamReader;
    import java.io.OutputStream;
    import java.io.PrintWriter;
    import java.nio.ByteBuffer;
    import java.util.Iterator;
    
    import javax.websocket.Session;
    
    import com.fasterxml.jackson.databind.JsonNode;
    import com.jcraft.jsch.Channel;
    import com.jcraft.jsch.ChannelShell;
    import com.jcraft.jsch.JSch;
    import com.jcraft.jsch.JSchException;
    
    import vip.r0n9.JsonUtil;
    import vip.r0n9.ws.WebSshHandler;
    
    public class SshSession {
    
        private Session websession;//从客户端发起的websocket连接
    
        private StringBuilder dataToDst = new StringBuilder();
    
        private JSch jsch = new JSch();//ssh客户端
    
        private com.jcraft.jsch.Session jschSession;//ssh服务端返回的单个客户端连接
    
        private ChannelShell channel;
    
        private InputStream inputStream;
        private BufferedReader stdout;
    
        private OutputStream outputStream;
        private PrintWriter printWriter;
        
        public SshSession() {}
        
        public SshSession(String hostname,int port,String username, String password, final Session session2, int rows, int cols) throws JSchException, IOException {
            this.websession = session2;
            jschSession = jsch.getSession(username, hostname, port);
            jschSession.setPassword(password);
            java.util.Properties config = new java.util.Properties();
            config.put("StrictHostKeyChecking", "no");
            jschSession.setConfig(config);
            jschSession.connect();
    
            channel = (ChannelShell) jschSession.openChannel("shell");
            channel.setPty(true);
            channel.setPtyType("xterm");
            channel.setPtySize(cols, rows, cols*8, rows*16);
            inputStream = channel.getInputStream();
            
            outputStream = channel.getOutputStream();
            printWriter = new PrintWriter(outputStream,false);
            channel.connect();
            
            outputStream.write("\r".getBytes());
            outputStream.flush();
            //这里可以用newFixedThreadPool线程池,可以更方便管理线程
            Thread thread = new Thread() {
    
                @Override
                public void run() {
    
                    try {
                        byte[] byteset = new byte[3072];
                        int res = inputStream.read(byteset);
                        if(res == -1)res = 0;
                        while (session2 != null && session2.isOpen()) { // 这里会阻塞,所以必须起线程来读取channel返回内容
                            ByteBuffer byteBuffer = ByteBuffer.wrap(byteset, 0, res);
                            synchronized (this) {
                                if(res != 0)
                                    session2.getBasicRemote().sendBinary(byteBuffer);
                            }
                            res = inputStream.read(byteset);
                            if(res == -1)res = 0;
                        }
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            };
            thread.start();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        public void close() {
            channel.disconnect();
            jschSession.disconnect();
            try {
                this.websession.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                this.inputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
            try {
                this.outputStream.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        
        public void getMessage(String message) throws IOException, JSchException {
    
            Session mysession = this.websession;
            System.out.println("来自客户端 " + mysession.getUserProperties().get("ClientIP") + " 的消息:" + message);
    
            JsonNode node = JsonUtil.strToJsonObject(message);
    
            if (node.has("resize")) {
                Iterator<JsonNode> myiter = node.get("resize").elements();
                int col = myiter.next().asInt();
                int row = myiter.next().asInt();
                channel.setPtySize(col, row, col*8, row*16);
                return;
            }
    
            if (node.has("data")) {
                String str = node.get("data").asText();
    
                outputStream.write(str.getBytes("utf-8"));
                outputStream.flush();
    
                return;
            }
    
        }
    
        public StringBuilder getDataToDst() {
            return dataToDst;
        }
    
        public OutputStream getOutputStream() {
            return outputStream;
        }
    
    }
    SshSession.java

      代理SSH客户端的核心逻辑在此,这里要注意不要用Reader和Writer,一些终端功能会无法运行。

      下载项目,开启Springboot,在浏览器上访问http://localhost:10003/,会进入登录页面,目前不支持RSA秘钥登录,只支持账号密码登录。

      客户端也可以用Vue.js实现:

    <template>
    </template>
     
    <script>
    import 'xterm/dist/xterm.css'
    import { Terminal } from 'xterm';
    import * as fit from 'xterm/dist/addons/fit/fit';
    import * as fullscreen from 'xterm/dist/addons/fullscreen/fullscreen'
    import openSocket from 'socket.io-client';
        export default {
          name: 'sshweb',
          props:['ip','port'],
            data () {
                return {
                wsServer:null,
                localip:'',
                localport:'',
                env: "",
                podName: "",
                contaName: "",
                logtxt: "",
                term:[0,0],
                colsLen:9,
                rowsLen:19,
                colRemain:21,
                msgId:0,
                col:80,
                row:24,
                 terminal: {
                        pid: 1,
                        name: 'terminal',
                        cols: 80,
                        rows: 24
                      },
                    
                }
            },
            watch:{
                port(val){
                this.localport=port;
                },
                ip(val){
                this.localip=ip;
                }
            },
            methods: {
                S4() {
                    return (((1+Math.random())*0x10000)|0).toString(16).substring(1);
                },
                guid() {
                    return (this.S4()+this.S4()+"-"+this.S4()+"-"+this.S4()+"-"+this.S4()+"-"+this.S4()+this.S4()+this.S4());
                },
                createServer1(){
                    this.msgId = this.guid();
                    var msgId = this.msgId;
                    var selfy = this;
                    var ipport = this.$route.params.ipport.split(':');
                    var myport = ipport[1];
                    var myip = ipport[0];
                    var wsurl = 'ws://127.0.0.1:10003/ssh/1?hostname=' + myip + '&port=' + myport + '&username=root&password=xunfang';
                    this.wsServer = new WebSocket(wsurl);
                    var myserver = this.wsServer;
                    let term = this.term[0];
                    term.on("data", function(data) {
                        var you = data;
                        if(you.length > 1)you = you[0];
                        console.log(you.charCodeAt());
                        myserver.send(JSON.stringify({'data': data}));
                    });
                    
                    myserver.onopen = function(evt) {
                        console.log(evt);
                    };
                    
                    myserver.onmessage = function(msg) {
                        var reader = new window.FileReader();
                        var isend = false;
    
                        reader.onloadend = function(){
                            var decoder = new window.TextDecoder('utf-8');
                            console.log(decoder);
                            var text = decoder.decode(reader.result);
                            console.log(text);
                            term.write(text);
                        };
                        
                        reader.readAsArrayBuffer(msg.data);
                    };
                    term.attachCustomKeyEventHandler(function(ev) {
                        if (ev.keyCode == 86 && ev.ctrlKey) {
                            myserver.send(JSON.stringify({'data': new TextEncoder().encode("\x00" + this.copy)}));
                        }
                    });
                },
                resize(row,col){
                    row = Math.floor(row/this.rowsLen);
                    col = Math.floor((col-this.colRemain)/this.colsLen);
                    if(row<24)row=24;
                    if(col<80)col=80;
                    if(this.row != row || this.col != col){
                        this.row=row;
                        this.col=col;
                        this.term[0].fit();
                        myserver.send(JSON.stringify({'resize': [cols, rows]}));
                    }
                }
            },
            mounted(){
                var selfy = this;
                window.onload = function(){
                    for(var i = 0;i < 1;i++){
                        var idname = 'net0';
                        Terminal.applyAddon(fit);
                        Terminal.applyAddon(fullscreen);
                        var terminalContainer = document.getElementById('app');
                        selfy.term = new Terminal({
                            cursorBlink: true
                        });
                        selfy.term.open(terminalContainer, true);
                        if(window.innerWidth > 0 && window.innerHeight > 0)
                            selfy.term.fit();
                        selfy.createServer1();
                    }
                }
                $(window).resize(() =>{
                    if(window.innerWidth > 0 && window.innerHeight > 0){
                        selfy.resize(window.innerHeight, window.innerWidth);
                    }
                });
            },
            components: {
            }
        }
    </script>
     
    <style scoped>
    #app{height:100%;width:100%;}
    </style>
    sshClient.vue

      两个项目的SSH窗口都是全屏的,只要窗口不小于某个大小,窗口的字会随着窗口缩放而调整位置。

      虚拟终端演示:

      

    哎...今天够累的,签到来了1...
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|小黑屋|Java自学者论坛 ( 声明:本站文章及资料整理自互联网,用于Java自学者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2024-11-15 12:33 , Processed in 0.080348 second(s), 29 queries .

    Powered by Discuz! X3.4

    Copyright © 2001-2021, Tencent Cloud.

    快速回复 返回顶部 返回列表