django webssh 模拟 putty 实现界面远程访问另一台服务器功能
原创博文 转载请注明出处!
团队需要做一个类似 putty 一样的远程 web page。不过办公室没人有做过这个东西所以只能自己摸索,后面终于做出了一个像一点样的、能够正常用的了,所在在这里记录一下中间遇到的坑
原理
socket
众所周知我们用到最多的就是 http, 但是 http 连接是一次性的、无状态的。而且只能由客户端发起。
而我们需要的连接是连续不间断的。就像聊天室一样,除非有人关闭聊天室,不然两方的人都能够一直 发送 / 接收 到对方的实时信息,不会断掉。所以我们需要的应该是 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 一样;
服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息。
使用
上述原理只做了简单介绍,需要深入了解请自行百度谷歌,网上一搜一大把资料,接下来我们将 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()