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()
posted @ 2019-09-24 15:22  Janey91  阅读(1005)  评论(0编辑  收藏  举报