目录
- 项目介绍和源码;
- 拿来即用的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: Upgradewebsocket连接安全和版本
Sec-WebSocket-Key: sN9cRrP/n9NdMgdcy2VJFQ==
Sec-WebSocket-Version: 13
websocket客户端(浏览器)和服务端交互过程如图3,由于websocket是基于TCP的,这里的握手只是应用层的关系,传输层已经保证了三次握手和四次挥手,每个客户端都可以主动暂停传输或者关闭传输。
实时日志
服务器一旦产生日志,就会传输给需要接收的客户端滚动显示,这样的逻辑利用websocket再好不过。这里给出图1所示的页面的html代码如下:
{% extends "./base.html" %}{% block othercss %}
<link href="https://cdn.datatables.net/1.10.16/css/jquery.dataTables.min.css" rel="stylesheet" />
{% endblock %}
{% block title %}{{ title }}{% endblock %}
{% block log %}{{ title }}{% endblock %}
{% block username %}{{ username }}{% endblock %}{% block mainbody %}
<section class="wrapper site-min-height"><h3><i class="fa fa-angle-right"></i>实时日志 <i class="fa fa-desktop"></i></h3><div class="row mt"><div class="form-panel"><div class="col-lg-12 row mt"><div class="col-sm-6"><h4 class="mb" style="float:left;dispaly:block;">实时日志</h4></div><div class="col-sm-6"><button type="button" class="btn btn-theme02" style="float:right" onclick="cleartable()"> 清空日志</button><input type="checkbox" onchange="isCheck(this)" style="float:left" data-toggle="switch"></div></div><div><table id="logtable" class="table-striped dataTable table-advance table-hover" style="word-break:break-all;"><thead><tr><th style="width:25%;">时间</th><th style="width:15%;">名字</th><th>内容</th></tr></thead><tbody id="log"></tbody></table></div></div></div>
</section>
{% endblock %}
它的javascipt代码主要是两部分逻辑,一部分是websocket相关的函数。另一部分是动态响应表格Datatable的控制代码,关于Datatable的使用会在文章查看服务器中的日志与前端的datatable的利用文章中介绍:
{% block scripts %}
<script>
$(document).ready(function (){// 动态响应表格的控制$('#logtable').DataTable({"scrollY": "670px", //让表格上下滚动,右边会出现滚动滑条"scrollCollapse": true,'columnDefs':[{'targets' : [1,2], //除时间列以外都不排序'orderable' : false}],"order": [[0 , "desc" ]],"paging": false, // 禁止分页"bInfo": false, //页脚信息"oLanguage": {"sZeroRecords": "打开按钮可以开始接收日志,日志默认为时间降序排列!","sSearch": "日志过滤:",},});
});
// 客户端websocket
var socket;
function init(){var host = "ws://127.0.0.1:8889/";try{// 建立一个websocketsocket = new WebSocket(host);// 打开websocketsocket.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<log.length; ++i){var logtime = log[i][0];var logname = log[i][1];var logcontent = log[i][2];table.row.add([logtime,logname,logcontent]).draw(true);}}// websocket关闭socket.onclose = function(){console.log('Lose Connection!');}}catch(ex){console(ex);}
}
function isCheck(obj){if($(obj).prop("checked")){init();}else{// 客户端发起关闭连接请求socket.send('quit');// 清空表格var table = $('#logtable').DataTable().clear().draw();}
}
function cleartable(){// 清空表格var table = $('#logtable').DataTable().clear().draw();
}
</script>
<!--custom switch-->
<script src="/templates/servermaterial/assets/js/bootstrap-switch.js"></script>
<!--custom tagsinput-->
<script src="/templates/servermaterial/assets/js/jquery.tagsinput.js"></script>
<!--custom checkbox & radio-->
<script src="/templates/servermaterial/assets/js/form-component.js"></script>
<script src="https://cdn.datatables.net/1.10.16/js/jquery.dataTables.min.js"></script>
{% 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_literalsfrom 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;"
,其目的是怕日志内容过多,超出表格,这里可以实现换行。
<table id="logtable" class="table-striped dataTable table-advance table-hover" 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{// 建立一个websocketsocket = new WebSocket(host);// 打开websocketsocket.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<log.length; ++i){var logtime = log[i][0];var logname = log[i][1];var logcontent = log[i][2];table.row.add([logtime,logname,logcontent]).draw(true);}}// websocket关闭socket.onclose = function(){console.log('Lose Connection!');}}catch(ex){console(ex);}
}
function isCheck(obj){if($(obj).prop("checked")){init();}else{// 客户端发起关闭连接请求socket.send('quit');// 清空表格var table = $('#logtable').DataTable().clear().draw();}
}
上面基本上已经把客户端写好了,下面来写下服务端。
服务器
服务器首先是接受浏览器的握手请求,然后解析数据。这里的服务端用多线程实现,服务端文件放在和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.txtsleep 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 jsonreload(sys)
sys.setdefaultencoding('utf-8')# 服务器解析浏览器发送的信息
def recv_data(conn):try:all_data = conn.recv(1024)if not len(all_data):return Falseexcept:passelse:code_len = ord(all_data[1]) & 127if 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 = 0for d in data:raw_str += chr(ord(d) ^ ord(masks[i % 4]))i += 1return raw_str# 服务器处理发送给浏览器的信息
def send_data(conn, data):if data:data = str(data)else:return Falsetoken = "\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 Falseprint ('%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] = valueif 'Sec-WebSocket-Key' not in headers:print ('%s : This socket is not websocket, client close.' % thread_name)conn.close()return FalseMAGIC_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 Truedef 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_pwdssh = login_server_by_pwd()# open channel pipelinetransport = ssh.get_transport()channel = transport.open_session()channel.get_pty()# execute commandcommand = 'tail -f /home/logs/log.txt'# out command into pipelinechannel.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()breakwhile 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_contentsend_data(conn, json.dumps(log))if channel.exit_status_ready():breakexcept: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 = 1print ('Websocket server start, wait for connect!')print '- - - - - - - - - - - - - - - - - - - - - - - - - - - - - -'while True:connection, address = sock.accept()thread_name = 'thread_%s' % indexprint ('%s : Connection from %s:%s' % (thread_name, address[0], address[1]))t = threading.Thread(target=getlog, args=(connection, address, thread_name))t.start()index += 1if __name__ == '__main__':wbservice()
其实这样的服务器实现方法存在很多的问题,因为每一个进程都会在服务器中开一个tail -f
的进程来处理实时日志,这里也没有用线程池处理,并且多线程并不是python中实现socket最好的方式,因为python中的多线程比较消耗资源,一般可以用协程或者epoll去解决(python中应尽量避免使用select,因为上限句柄1024很容易用完,上限改起来很麻烦),关于协程可以移步至文章协程及Python中的协程。因为这个工具的并发量很小,没多少人用,就没有对其优化了。
结语
本文简单的介绍了websocket的原理和基于websocket实时日志的实现,附带说了下前端怎么把动态响应表格改造成实时日志的滚动效果。希望对有需要的童鞋有帮助。