一、開始的話
使用python實現websocket服務器,可以在瀏覽器上實時顯示遠程服務器的日志。
之前寫了一個發布系統,每次發布版本后,為了了解發布情況(進度、是否有錯誤)都會登錄到服務器上查看日志,有點麻煩,如果發布的服務器比較多,難道要登錄到每台服務器去看日志嗎?作為新時代的運維,太不能接收這種重復操作的體力勞動了,於是一個看日志的功能就這么誕生了。下面是效果圖,頁面丑陋不堪,將就着吧。
二、行動
打開頁面時,自動連接websocket服務器,完成握手,並發送ip和type給服務端,所以可以看不同類型,不同機器上的日志,websocket服務器接收到信息后,去數據庫查找對應的日志路徑和主機賬號密碼,然后起一個線程ssh登錄到遠程服務器上tail查看日志,再推送給瀏覽器,代碼如下:
1 # coding:utf-8 2 import os 3 import struct 4 import base64 5 import hashlib 6 import socket 7 import threading 8 import paramiko 9 10 11 def get_ssh(ip, user, pwd): 12 try: 13 ssh = paramiko.SSHClient() 14 ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) 15 ssh.connect(ip, 22, user, pwd, timeout=15) 16 return ssh 17 except Exception, e: 18 print e 19 return "False" 20 21 22 def recv_data(conn): # 服務器解析瀏覽器發送的信息 23 try: 24 all_data = conn.recv(1024) 25 if not len(all_data): 26 return False 27 except: 28 pass 29 else: 30 code_len = ord(all_data[1]) & 127 31 if code_len == 126: 32 masks = all_data[4:8] 33 data = all_data[8:] 34 elif code_len == 127: 35 masks = all_data[10:14] 36 data = all_data[14:] 37 else: 38 masks = all_data[2:6] 39 data = all_data[6:] 40 raw_str = "" 41 i = 0 42 for d in data: 43 raw_str += chr(ord(d) ^ ord(masks[i % 4])) 44 i += 1 45 return raw_str 46 47 48 def send_data(conn, data): # 服務器處理發送給瀏覽器的信息 49 if data: 50 data = str(data) 51 else: 52 return False 53 token = "\x81" 54 length = len(data) 55 if length < 126: 56 token += struct.pack("B", length) # struct為Python中處理二進制數的模塊,二進制流為C,或網絡流的形式。 57 elif length <= 0xFFFF: 58 token += struct.pack("!BH", 126, length) 59 else: 60 token += struct.pack("!BQ", 127, length) 61 data = '%s%s' % (token, data) 62 conn.send(data) 63 return True 64 65 66 def handshake(conn, address, thread_name): # 握手建立連接 67 headers = {} 68 shake = conn.recv(1024) 69 if not len(shake): 70 return False 71 72 print ('%s : Socket start handshaken with %s:%s' % (thread_name, address[0], address[1])) 73 header, data = shake.split('\r\n\r\n', 1) 74 for line in header.split('\r\n')[1:]: 75 key, value = line.split(': ', 1) 76 headers[key] = value 77 78 if 'Sec-WebSocket-Key' not in headers: 79 print ('%s : This socket is not websocket, client close.' % thread_name) 80 conn.close() 81 return False 82 83 MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' 84 HANDSHAKE_STRING = "HTTP/1.1 101 Switching Protocols\r\n" \ 85 "Upgrade:websocket\r\n" \ 86 "Connection: Upgrade\r\n" \ 87 "Sec-WebSocket-Accept: {1}\r\n" \ 88 "WebSocket-Origin: {2}\r\n" \ 89 "WebSocket-Location: ws://{3}/\r\n\r\n" 90 91 sec_key = headers['Sec-WebSocket-Key'] 92 res_key = base64.b64encode(hashlib.sha1(sec_key + MAGIC_STRING).digest()) 93 str_handshake = HANDSHAKE_STRING.replace('{1}', res_key).replace('{2}', headers['Origin']).replace('{3}', headers['Host']) 94 conn.send(str_handshake) # 發送建立連接的信息 95 print ('%s : Socket handshaken with %s:%s success' % (thread_name, address[0], address[1])) 96 print 'Start transmitting data...' 97 print '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -' 98 return True 99 100 101 def dojob(conn, address, thread_name): 102 handshake(conn, address, thread_name) # 握手 103 conn.setblocking(0) # 設置socket為非阻塞 104 105 ssh = get_ssh('192.168.1.1', 'root', '123456') # 連接遠程服務器(日志所在的服務器) 106 ssh_t = ssh.get_transport() 107 chan = ssh_t.open_session() 108 chan.setblocking(0) # 設置非阻塞 109 chan.exec_command('tail -f /var/log/messages') # 執行查看日志命令 110 111 while True: # 下面這個邏輯是在看日志時能停止或繼續,寫的有點混亂,還沒想到優化的方案 112 clientdata = recv_data(conn) 113 if clientdata is not None and 'quit' in clientdata: # 當瀏覽器點擊stop按鈕或close按鈕時,斷開連接 114 print ('%s : Socket close with %s:%s' % (thread_name, address[0], address[1])) 115 send_data(conn, 'close connect') 116 conn.close() 117 break 118 while True: 119 while chan.recv_ready(): 120 clientdata1 = recv_data(conn) 121 if clientdata1 is not None and 'quit' in clientdata1: 122 print ('%s : Socket close with %s:%s' % (thread_name, address[0], address[1])) 123 send_data(conn, 'close connect') 124 conn.close() 125 break 126 log_msg = chan.recv(10000).strip() # 接收日志信息 127 print log_msg 128 send_data(conn, log_msg) 129 if chan.exit_status_ready(): 130 break 131 clientdata2 = recv_data(conn) 132 if clientdata2 is not None and 'quit' in clientdata2: 133 print ('%s : Socket close with %s:%s' % (thread_name, address[0], address[1])) 134 send_data(conn, 'close connect') 135 conn.close() 136 break 137 break 138 139 140 def ws_service(): 141 142 index = 1 143 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) 144 sock.bind(("127.0.0.1", 12345)) 145 sock.listen(100) 146 147 print ('\r\n\r\nWebsocket server start, wait for connect!') 148 print '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -' 149 while True: 150 connection, address = sock.accept() 151 thread_name = 'thread_%s' % index 152 print ('%s : Connection from %s:%s' % (thread_name, address[0], address[1])) 153 t = threading.Thread(target=dojob, args=(connection, address, thread_name)) 154 t.start() 155 index += 1 156 157 158 ws_service()
頁面代碼如下:
<!DOCTYPE html>
<html>
<head>
<title>WebSocket</title>
<style>
#log {
width: 440px;
height: 200px;
border: 1px solid #7F9DB9;
overflow: auto;
}
pre {
margin: 0 0 0;
padding: 0;
border: hidden;
background-color: #0c0c0c;
color: #00ff00;
}
#btns {
text-align: right;
}
</style>
<script>
var socket;
function init() {
var host = "ws://127.0.0.1:12345/";
try {
socket = new WebSocket(host);
socket.onopen = function () {
log('Connected');
};
socket.onmessage = function (msg) {
log(msg.data);
var obje = document.getElementById("log"); //日志過多時清屏
var textlength = obje.scrollHeight;
if (textlength > 10000) {
obje.innerHTML = '';
}
};
socket.onclose = function () {
log("Lose Connection!");
$("#start").attr('disabled', false);
$("#stop").attr('disabled', true);
};
$("#start").attr('disabled', true);
$("#stop").attr('disabled', false);
}
catch (ex) {
log(ex);
}
}
window.onbeforeunload = function () {
try {
socket.send('quit');
socket.close();
socket = null;
}
catch (ex) {
log(ex);
}
};
function log(msg) {
var obje = document.getElementById("log");
obje.innerHTML += '<pre><code>' + msg + '</code></pre>';
obje.scrollTop = obje.scrollHeight; //滾動條顯示最新數據
}
function stop() {
try {
log('Close connection!');
socket.send('quit');
socket.close();
socket = null;
$("#start").attr('disabled', false);
$("#stop").attr('disabled', true);
}
catch (ex) {
log(ex);
}
}
function closelayer() {
try {
log('Close connection!');
socket.send('quit');
socket.close();
socket = null;
}
catch (ex) {
log(ex);
}
var index = parent.layer.getFrameIndex(window.name); //先得到當前iframe層的索引
parent.layer.close(index); //再執行關閉
}
</script>
</head>
<body onload="init()">
<div class="row">
<div class="col-lg-12">
<div id="log" style="width: 100%;height:440px;background-color: #0c0c0c;overflow:scroll;overflow-x: auto;"></div>
<br>
</div>
</div>
<div class="row">
<div class="col-lg-12">
<div id="btns">
<input disabled="disabled" type="button" class="btn btn-primary btn-sm" value="start" id="start" onclick="init()">
<input disabled="disabled" type="button" class="btn btn-primary btn-sm" value="stop" id="stop" onclick="stop()" >
<input type="button" class="btn btn-primary btn-sm" value="close" id="close" onclick="closelayer()" >
</div>
</div>
</div>
</body>
</html>