目录
- 项目介绍和源码;
- 拿来即用的bootstrap模板;
- 服务器SSH服务配置与python中paramiko的使用;
- 用户登陆与session;
- 最简单的实践之修改服务器时间;
- 查看和修改服务器配置与数据库的路由;
- 基于websocket的实时日志实现;
- 查看服务器中的日志与前端的datatable的利用;
- 重启服务器进程。
前言
实时日志的查看需要用到websocket,这篇文章会说说如何利用websocket实现一个实时日志查看页面。页面如图1所示。在这个功能里,网页的页面是通过server/views.py中的函数渲染的,但是服务器是单独用python写的websocket服务器,客户端浏览器单独进行链接。
Websocket原理
文章WebSocket 通信过程与实现已经把websocket的原理和和使用方法介绍的很详细了。项目尝试过利用HTTP去实现一个实时日志的功能,但是由于HTTP是被动的,客户端要不停的发起HTTP请求到服务端,然后服务端从存储日志临时内容的中间件(redis等)中拿给客户端刚更新的日志,如图2的实现逻辑,这样不仅浪费资源而且实现起来也挺费劲。
WebSocket便可以做到服务器向客户端主动推送数据,这样服务器一旦更新了日志,就可以主动推送日志到客户端上,浏览器的客户端通过一些轻量封装的socket函数实现创建、传输、关闭等功能。WebSocket是HTML5中的协议,现在一般主流的浏览器都会支持该协议。
WebSocket协议借用了HTTP的协议来完成一部分和服务端的握手,握手之后客户端和服务端就可以相互传输数据了(全双工通信)。客户端发送的握手协议如下:
GET / HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Host: example.com
Origin: http://example.com
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
与HTTP报文不一样的是加入的websocket独有的部分:
客户端发起的是websocket连接
Upgrade: websocket
Connection: Upgrade
websocket连接安全和版本
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
websocket客户端(浏览器)和服务端交互过程如图3,由于websocket是基于TCP的,这里的握手只是应用层的关系,传输层已经保证了三次握手和四次挥手,每个客户端都可以主动暂停传输或者关闭传输。
实时日志
服务器一旦产生日志,就会传输给需要接收的客户端滚动显示,这样的逻辑利用websocket再好不过。这里给出图1所示的页面的html代码如下:
{% extends "./base.html" %}
{% block othercss %}
{% endblock %}
{% block title %}{{ title }}{% endblock %}
{% block log %}{{ title }}{% endblock %}
{% block username %}{{ username }}{% endblock %}
{% block mainbody %}
实时日志
实时日志
时间
名字
内容
{% endblock %}
它的javascipt代码主要是两部分逻辑,一部分是websocket相关的函数。另一部分是动态响应表格Datatable的控制代码,关于Datatable的使用会在文章查看服务器中的日志与前端的datatable的利用文章中介绍:
{% block scripts %}
{% endblock %}
分别添加一个url和view函数用来显示这个页面,分别写在server/urls.py和server/views.py中,最后显示的界面就是图1的页面了:
- url转到views中的realtimelog渲染函数
url(r'^realtimelog', views.realtimelog),
- server/views.py的realtimelog
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.contrib.auth import logout
from django.shortcuts import render_to_response
from django.http import JsonResponse
from django.contrib.auth.decorators import login_required
import json
import time
@login_required(login_url='/loginpage')
def realtimelog(request):
username = request.session.get('username')
pagedict = {'title': htmltitle, 'username': username}
return render_to_response("servermaterial/realtimelog.html", pagedict)
客户端界面
- Datatable使用
使用Datatable的好处是,这个现成的动态响应表格几乎已经集成好了表格中需要的所有功能,我们把Datatable改造一下就能够支持实时的滚动显示日志新内容这个需求,Datatable的使用可以参考这篇Datetable 中文网和这篇CSDN博客,这里说下如何改造一下现有的Datatable变成页面需求的样子。
注意到html代码中的table属性里面有个style="word-break:break-all;"
,其目的是怕日志内容过多,超出表格,这里可以实现换行。
其javascript代码中,字段含义已经注释。
$(document).ready(function (){
// 动态响应表格的控制
$('#logtable').DataTable({
"scrollY": "670px", //让表格上下滚动,右边会出现滚动滑条,限定表格的高度为670px,如图4蓝框
"scrollCollapse": true,
'columnDefs':[{
'targets' : [1,2], //除时间列以外都不排序
'orderable' : false //1列、2列不排序(名字、内容列)
}],
"order": [[0 , "desc" ]], //按日志时间降序,如图4红框
"paging": false, //禁止分页
"bInfo": false, //页脚信息
"oLanguage": {
"sZeroRecords": "打开按钮可以开始接收日志,日志默认为时间降序排列!", //表格为空时的默认显示信息,如图5绿框
"sSearch": "日志过滤:", //右上角的搜索,图5红框
},
});
});
- websocket客户端编写
其中用到了三个函数,分别是socket.onopen
,socket.onmessage
,socket.onclose
,分别用于打开、传输和关闭,这里把他们写到了日志开关(图6红框所示)上,打开开关的时候执行init()函数创建一个websocket进行通信,关闭的时候给服务端发送一个quit,并清空表格中的数据。
var socket;
function init(){
var host = "ws://127.0.0.1:8889/";
try{
// 建立一个websocket
socket = new WebSocket(host);
// 打开websocket
socket.onopen = function(){
console.log('Connected');
server_tag = $('.logo').text();
socket.send(server_tag);
};
// 监听接收服务端的消息
socket.onmessage = function(msg){
// 如果收到服务端的Bye,关闭客户端的
if(eval(msg.data) == 'Bye'){
socket.close();
socket = null;
return ;
}
var table = $('#logtable').DataTable();
var log = eval(msg.data);
for(i=0; i
上面基本上已经把客户端写好了,下面来写下服务端。
服务器
服务器首先是接受浏览器的握手请求,然后解析数据。这里的服务端用多线程实现,服务端文件放在和funtions.py同级的目录下,即WebTool/WebTool,如图7红框。
通过websocket,每一个客户端的请求都会被服务端线程处理,每一个线程中都会利用paramiko在服务器相应的log目录下tail -f日志获得刷新。
为了服务器有日志的输出,我们在Linux服务器的home/logs目录下写一个不断生成日志新内容伪造日志生成源的shell脚本autogenlog.sh,生成的日志的格式是:[时间][名字],{日志内容},控制其每两秒钟在log.txt中追加一条日志记录。形如:
[2018-05-06 23:05:28][Error],{这里是一段测试的内容,服务器的日志内容通过websocket主动推送到浏览器上}
#!/bin/sh
while true
do
# 获取系统的时间
logDate=$(date "+%Y-%m-%d %H:%M:%S")
echo [$logDate][Error],{这里是一段测试的内容,服务器的日志内容通过websocket主动推送到浏览器上} >> log.txt
sleep 2
done
服务器代码如下,recv_data
函数用于解析浏览器的信息,send_data
用于发送给浏览器的信息,handshake
函数用来和浏览器之间握手建立连接。在函数getlog
里面,command变量存放执行的命令tail -f /home/logs/log.txt
,而这个log.txt中增加的日志记录是通过上面给出的shell脚本追加的。正则表达式"\[(.*?)\]\[(.*?)\],({.*})"
用来提取日志的时间,名字和内容,经由send_data
传递给浏览器滚动显示,关于paramiko的使用请移步至文章服务器SSH服务配置与python中paramiko的使用。最后服务器的输出为图9所示。
# -*- coding: utf-8 -*-
import struct
import base64
import hashlib
import socket
import threading
import re
import sys
import json
reload(sys)
sys.setdefaultencoding('utf-8')
# 服务器解析浏览器发送的信息
def recv_data(conn):
try:
all_data = conn.recv(1024)
if not len(all_data):
return False
except:
pass
else:
code_len = ord(all_data[1]) & 127
if code_len == 126:
masks = all_data[4:8]
data = all_data[8:]
elif code_len == 127:
masks = all_data[10:14]
data = all_data[14:]
else:
masks = all_data[2:6]
data = all_data[6:]
raw_str = ""
i = 0
for d in data:
raw_str += chr(ord(d) ^ ord(masks[i % 4]))
i += 1
return raw_str
# 服务器处理发送给浏览器的信息
def send_data(conn, data):
if data:
data = str(data)
else:
return False
token = "\x81"
length = len(data)
if length < 126:
token += struct.pack("B", length)
elif length <= 0xFFFF:
token += struct.pack("!BH", 126, length)
else:
token += struct.pack("!BQ", 127, length)
# struct为Python中处理二进制数的模块,二进制流为C,或网络流的形式。
data = '%s%s' % (token, data)
conn.send(data)
return True
# 握手
def handshake(conn, address, thread_name):
headers = {}
shake = conn.recv(1024)
if not len(shake):
return False
print ('%s : Socket start handshaken with %s:%s' % (thread_name, address[0], address[1]))
header, data = shake.split('\r\n\r\n', 1)
for line in header.split('\r\n')[1:]:
key, value = line.split(': ', 1)
headers[key] = value
if 'Sec-WebSocket-Key' not in headers:
print ('%s : This socket is not websocket, client close.' % thread_name)
conn.close()
return False
MAGIC_STRING = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
HANDSHAKE_STRING = "HTTP/1.1 101 Switching Protocols\r\n" \
"Upgrade:WebSocket\r\n" \
"Connection: Upgrade\r\n" \
"Sec-WebSocket-Accept: {1}\r\n" \
"WebSocket-Location: ws://{2}/chat\r\n" \
"WebSocket-Protocol:chat\r\n\r\n"
sec_key = headers['Sec-WebSocket-Key']
res_key = base64.b64encode(hashlib.sha1(sec_key + MAGIC_STRING).digest())
str_handshake = HANDSHAKE_STRING.replace('{1}', res_key).replace('{2}', headers['Origin']).replace('{3}',
headers['Host'])
conn.send(str_handshake)
print ('%s : Socket handshaken with %s:%s success' % (thread_name, address[0], address[1]))
print 'Start transmitting data...'
print '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -'
return True
def getlog(conn, address, thread_name):
handshake(conn, address, thread_name) # 握手
server_name = recv_data(conn)
print 'connect to ' + unicode(server_name)
conn.setblocking(0) # 设置socket为非阻塞
from functions import login_server_by_pwd
ssh = login_server_by_pwd()
# open channel pipeline
transport = ssh.get_transport()
channel = transport.open_session()
channel.get_pty()
# execute command
command = 'tail -f /home/logs/log.txt'
# out command into pipeline
channel.exec_command(command)
while True:
try:
clientdata = recv_data(conn)
if clientdata is not None and 'quit' in clientdata:
print ('%s : Socket close with %s:%s' % (thread_name, address[0], address[1]))
send_data(conn, json.dumps('Bye'))
ssh.close()
channel.close()
conn.close()
break
while channel.recv_ready():
recvfromssh = channel.recv(16371)
log = re.findall("\[(.*?)\]\[(.*?)\],({.*})", recvfromssh)
if len(log):
# log_time, log_name, log_content = log[0][0], log[0][1], log[0][2]
# print log_time, log_name, log_content
send_data(conn, json.dumps(log))
if channel.exit_status_ready():
break
except:
print ('%s : Socket close with %s:%s' % (thread_name, address[0], address[1]))
ssh.close()
channel.close()
conn.close()
channel.close()
ssh.close()
def wbservice():
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.bind(("0.0.0.0", 8889))
sock.listen(100)
index = 1
print ('Websocket server start, wait for connect!')
print '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -'
while True:
connection, address = sock.accept()
thread_name = 'thread_%s' % index
print ('%s : Connection from %s:%s' % (thread_name, address[0], address[1]))
t = threading.Thread(target=getlog, args=(connection, address, thread_name))
t.start()
index += 1
if __name__ == '__main__':
wbservice()
其实这样的服务器实现方法存在很多的问题,因为每一个进程都会在服务器中开一个tail -f
的进程来处理实时日志,这里也没有用线程池处理,并且多线程并不是python中实现socket最好的方式,因为python中的多线程比较消耗资源,一般可以用协程或者epoll去解决(python中应尽量避免使用select,因为上限句柄1024很容易用完,上限改起来很麻烦),关于协程可以移步至文章协程及Python中的协程。因为这个工具的并发量很小,没多少人用,就没有对其优化了。
结语
本文简单的介绍了websocket的原理和基于websocket实时日志的实现,附带说了下前端怎么把动态响应表格改造成实时日志的滚动效果。希望对有需要的童鞋有帮助。