原創博文 轉載請注明出處!
團隊需要做一個類似 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()