django webssh 模擬 putty 實現界面遠程訪問另一台服務器功能


原創博文 轉載請注明出處!

團隊需要做一個類似 putty 一樣的遠程 web page。不過辦公室沒人有做過這個東西所以只能自己摸索,后面終於做出了一個像一點樣的、能夠正常用的了,所在在這里記錄一下中間遇到的坑

原理

socket

眾所周知我們用到最多的就是 http, 但是 http 連接是一次性的、無狀態的。而且只能由客戶端發起。

而我們需要的連接是連續不間斷的。就像聊天室一樣,除非有人關閉聊天室,不然兩方的人都能夠一直 發送 / 接收 到對方的實時信息,不會斷掉。所以我們需要的應該是 socket 連接。

原理如下圖。

socket

建立Socket連接至少需要一對套接字,其中一個運行於客戶端,稱為ClientSocket ,另一個運行於服務器端,稱為ServerSocket 。

套接字之間的連接過程分為三個步驟:服務器監聽,客戶端請求,連接確認。

1。服務器監聽:服務器端套接字並不定位具體的客戶端套接字,而是處於等待連接的狀態,實時監控網絡狀態,等待客戶端的連接請求。

2。客戶端請求:指客戶端的套接字提出連接請求,要連接的目標是服務器端的套接字。為此,客戶端的套接字必須首先描述它要連接的服務器的套接字,指出服務器端套接字的地址和端口號,然后就向服務器端套接字提出連接請求。

3。連接確認:當服務器端套接字監聽到或者說接收到客戶端套接字的連接請求時,就響應客戶端套接字的請求,建立一個新的線程,把服務器端套接字的描述發給客戶端,一旦客戶端確認了此描述,雙方就正式建立連接。而服務器端套接字繼續處於監聽狀態,繼續接收其他客戶端套接字的連接請求。

注意

Socket 是對 TCP/IP 協議的封裝, Socket 本身並不是協議,而是一個調用接口( API ),通過 Socket ,我們才能使用 TCP/IP 協議。
Socket 的出現只是使得程序員更方便地使用 TCP/IP 協議棧而已,是對 TCP/IP 協議的抽象,從而形成了我們知道的一些最基本的函數接口。

引用出處

socket 編程:

# client

# server

而由於我們需要與前端做交互,所以我們需要用到的是 websocket:

兩者區別在於:
Socket是傳輸控制層協議,WebSocket是應用層協議。

websocket

WebSocket 是 HTML5 一種新的協議。
它實現了瀏覽器與服務器全雙工通信,能更好的節省服務器資源和帶寬並達到實時通訊。
它建立在 TCP 之上,同 HTTP 一樣通過 TCP 來傳輸數據,但是它和 HTTP 最大不同是:
WebSocket 是一種雙向通信協議,在建立連接后,WebSocket 服務器和 Browser/Client Agent 都能主動的向對方發送或接收數據,就像 Socket 一樣;

引用出處

websocket
服務器可以主動向客戶端推送信息,客戶端也可以主動向服務器發送信息。

使用

上述原理只做了簡單介紹,需要深入了解請自行百度谷歌,網上一搜一大把資料,接下來我們將 code 是如何實現的。

客戶端

  • 前端 js
var ws = new WebSocket("wss://echo.websocket.org"); // 與后台連接

ws.onopen = function(evt) {             // 當與后台連接成功時
  console.log("Connection open ..."); 
  ws.send("Hello WebSockets!");
};

ws.onmessage = function(evt) {          // 當接收到后台數據時
  console.log( "Received Message: " + evt.data);
  ws.close();
};

ws.onclose = function(evt) {            // 當連接結束時
  console.log("Connection closed.");
};  

當然,這是最簡單的前端 websocket code, 而當 ws.onmessage 接收到的后台信息會伴隨着<-[01;34m這樣的顏色屬性等字符。

[root@localhost ~]#ls
anaconda-ks,cfg  <-[0m<-[01;34mdino<-[0m  <-[01;34mgrub<-[0m
<-[01;34mDecktop<-[0m  <-[01;34mDocuments<-[0m

需要搭配前端插件 xterm.js 才能使前端呈現出遠程終端的效果。

注意這里有一個坑,我前面理解有誤導致連着后台的 code 都寫錯了浪費了很多時間。
我們搭配 xterm 使用的 code 是這樣的

    var terminal = document.getElementById('term_dsp');
    var term = new window.Terminal({
        cursorBlink: true,
        // term 其他配置項...
    });

    term.on('data', function(data) {    // 當屏幕有輸入
        sock.send(data);
        // sock.send(JSON.stringify({'data': data}));
    });

    sock.onopen = function() {          // 當遠程連接成功
        term.open(terminal, true);      // 打開前端模擬終端界面
        // term.toggleFullscreen(true);
    };

    sock.onmessage = function(msg) {    // 當遠程接收到信息
        term.write(msg.data);           
    };

    sock.onerror = function(e) {
        console.log(e);
    };
    
    sock.onclose = function(e) {
        // ...
    };

這里當我們在 term 輸入數據時,你輸入的每一個字符每一個鍵值都是傳到后台,由后台傳到另一台遠程終端。
然后,從遠程終端上抓到輸出回傳到后台,后台回傳到前端顯示的。即你在前端的終端中輸入一個a, 你看到它顯示在界面終端上了,但其實它是經歷了一個過程之后從遠程終端回來的。而不是像輸入框那樣,你打一個字段就顯示在上面一樣。
天知道這個坑了我多久,狗帶!


服務端

  • 后端 django

與前端客戶端的交互就是這個簡單:

    uwsgi.websocket_handshake()     # connect with client

    while True:
        cmd = uwsgi.websocket_recv()    # 阻塞,直到接收到客戶端的信息 cmd
        data = ''
        # ...
        uwsgi.websocket_send(data)  # 回傳數據給客戶端

但是需要做的功能是遠程到另一個終端,后台是還有一個與另一個遠程終端交互的功能的。
這里我們使用的 paramiko, channal 去遠程連接另一個終端。

    ...
    uwsgi.websocket_handshake()     # connect with client
    if (host != None and port != None and username != None and password != None):
        try:
            ssh = paramiko.SSHClient()
            ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
            ssh.connect(host, port=port, username=username, password=password)
        except socket.error:
            uwsgi.websocket_send('Unable to connect to addr.\r\n')  # term.js模擬linux終端識別字符方式 注意\n與\r\n
            raise ValueError('Unable to connect to addr')   # in logging
        except paramiko.BadAuthenticationType:
            uwsgi.websocket_send('SSH authentication failed.\r\n')
            raise ValueError('SSH authentication failed.')
        except paramiko.ssh_exception.AuthenticationException:
            uwsgi.websocket_send('SSH authentication failed.\r\n')
            raise ValueError('SSH authentication failed.')
        except paramiko.BadHostKeyException:
            uwsgi.websocket_send('Bad host key.\r\n')
            raise ValueError('Bad host key.')
        else:
            uwsgi.websocket_send('connect success.\r\n')

        channel = ssh.invoke_shell()        # 建立交互式 shell 連接
        while True:
            cmd = uwsgi.websocket_recv()    # 阻塞,接收到客戶端的信息才繼續往下執行
            try:
                out = ''
                channel.send(cmd)
                rec = channel.recv(9999)    # 注意這里也是有阻塞的,即若接收不到遠程回來的信息就不繼續往下執行
                out = rec.decode('ascii')   # python3中,編碼的時候區分了字符串和二進制 encode 改為 decode
                uwsgi.websocket_send(out)   # 回傳數據給客戶端
            except Exception as e:
                logger.info("ssh execute command error: %s", e)

上面的 code 實現了遠程連接的基本功能,但是會有一個問題,就是 ping 這種需要不斷回傳數據回前端的指令無法實時回傳,需要你"踢一下才會回傳一下"。

問題出在

        while True:
            cmd = uwsgi.websocket_recv()    # 阻塞,接收到客戶端的信息才繼續往下執行
            try:
                out = ''
                channel.send(cmd)           # 
                rec = channel.recv(9999)    # 注意這里也是有阻塞的,即若接收不到遠程回來的信息就不繼續往下執行
                out = rec.decode('ascii')   # python3中,編碼的時候區分了字符串和二進制 encode 改為 decode
                uwsgi.websocket_send(out)   # 回傳數據給客戶端
            except Exception as e:
                logger.info("ssh execute command error: %s", e)

這段 code 的邏輯就是:
while True: 不斷循環等待 cmd = uwsgi.websocket_recv() 接收到前端傳過來的數據。當有數據過來時,
channel.send(cmd)將數據傳送到遠程的另一台終端,
rec = channel.recv(9999)去遠程的另一台終端拿終端的數據。

所以才會有"踢一下,回傳一下。踢一下,回傳一下。"的現象,而我們要的是像 ping 這樣的指令,它會主動從后台不斷的回傳信息回來。

所以我們結合上面的思路:

while True:
    # 阻塞 等待接收數據

可以想到,我們應該做的是給后台開線程。

  • 一個一直等待接收前端傳過來的數據,接收到數據之后發送給遠程的另一台終端。
def send_cmd_from_front_end(channel):
    while True:
        cmd = uwsgi.websocket_recv()
        try:
            channel.send(cmd)
        except Exception as e:
            logger.info("send_cmd_from_front_end error: %s", e)
  • 一個一直等待等待接收遠程的另一台終端傳回來的數據,接收到之后立馬傳回前端。
def recv_from_remote(channel):
    while True:
        try:
            time.sleep(0.1)             
            rec = channel.recv(9999)    # 若后面無數據 阻塞直至能拿到數據
            out = rec.decode('ascii')   
            uwsgi.websocket_send(out)   # 回傳數據給客戶端
        except Exception as e:
            logger.info("recv_from_remote error: %s", e)

所以后台的 code 應該是這樣:

def ssh_remote(host, port, username, password, uwsgi, request):
    try:
        ssh = paramiko.SSHClient()
        ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
        ssh.connect(host, port=port, username=username, password=password)
    except socket.error:
        uwsgi.websocket_send('Unable to connect to addr.\r\n')  # term.js模擬linux終端識別字符方式 注意\n與\r\n
        raise ValueError('Unable to connect to addr')           # in logging
    except paramiko.BadAuthenticationType:
        uwsgi.websocket_send('SSH authentication failed.\r\n')
        raise ValueError('SSH authentication failed.')
    except paramiko.ssh_exception.AuthenticationException:
        uwsgi.websocket_send('SSH authentication failed.\r\n')
        raise ValueError('SSH authentication failed..')
    except paramiko.BadHostKeyException:
        uwsgi.websocket_send('Bad host key.\r\n')
        raise ValueError('Bad host key.')
    else:
        uwsgi.websocket_send('connect success.\r\n')

    channel = ssh.invoke_shell()        # 建立交互式 shell 連接
    send_cmd = threading.Thread(target=send_cmd_from_front_end, args=(channel,))
    recv_data = threading.Thread(target=recv_from_remote, args=(channel))
    send_cmd.start()
    recv_data.start()
    send_cmd.join()
    recv_data.join()


免責聲明!

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



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