如何解決Vue.js里面noVNC的截圖問題之后篇——用web虛擬終端作為替代功能


  使用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[i] = new Terminal({
                        cursorBlink: true
                    });
                    selfy.term[i].open(terminalContainer, true);
                    if(window.innerWidth > 0 && window.innerHeight > 0)
                        selfy.term[i].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[i] = new Terminal({
                        cursorBlink: true
                    });
                    selfy.term[i].open(terminalContainer, true);
                    if(window.innerWidth > 0 && window.innerHeight > 0)
                        selfy.term[i].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窗口都是全屏的,只要窗口不小於某個大小,窗口的字會隨着窗口縮放而調整位置。

  虛擬終端演示:

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM