django搭建一个小型的服务器运维网站-基于websocket的实时日志实现

目录

  1. 项目介绍和源码;
  2. 拿来即用的bootstrap模板;
  3. 服务器SSH服务配置与python中paramiko的使用;
  4. 用户登陆与session;
  5. 最简单的实践之修改服务器时间;
  6. 查看和修改服务器配置与数据库的路由;
  7. 基于websocket的实时日志实现;
  8. 查看服务器中的日志与前端的datatable的利用;
  9. 重启服务器进程。

前言

  实时日志的查看需要用到websocket,这篇文章会说说如何利用websocket实现一个实时日志查看页面。页面如图1所示。在这个功能里,网页的页面是通过server/views.py中的函数渲染的,但是服务器是单独用python写的websocket服务器,客户端浏览器单独进行链接。

django搭建一个小型的服务器运维网站-基于websocket的实时日志实现_第1张图片

Websocket原理

  文章WebSocket 通信过程与实现已经把websocket的原理和和使用方法介绍的很详细了。项目尝试过利用HTTP去实现一个实时日志的功能,但是由于HTTP是被动的,客户端要不停的发起HTTP请求到服务端,然后服务端从存储日志临时内容的中间件(redis等)中拿给客户端刚更新的日志,如图2的实现逻辑,这样不仅浪费资源而且实现起来也挺费劲。

django搭建一个小型的服务器运维网站-基于websocket的实时日志实现_第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的,这里的握手只是应用层的关系,传输层已经保证了三次握手和四次挥手,每个客户端都可以主动暂停传输或者关闭传输。

django搭建一个小型的服务器运维网站-基于websocket的实时日志实现_第3张图片

实时日志

  服务器一旦产生日志,就会传输给需要接收的客户端滚动显示,这样的逻辑利用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红框
        },
    });
});

django搭建一个小型的服务器运维网站-基于websocket的实时日志实现_第4张图片

django搭建一个小型的服务器运维网站-基于websocket的实时日志实现_第5张图片

  • websocket客户端编写

  其中用到了三个函数,分别是socket.onopensocket.onmessagesocket.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

django搭建一个小型的服务器运维网站-基于websocket的实时日志实现_第6张图片

  上面基本上已经把客户端写好了,下面来写下服务端。

服务器

  服务器首先是接受浏览器的握手请求,然后解析数据。这里的服务端用多线程实现,服务端文件放在和funtions.py同级的目录下,即WebTool/WebTool,如图7红框。

django搭建一个小型的服务器运维网站-基于websocket的实时日志实现_第7张图片

  通过websocket,每一个客户端的请求都会被服务端线程处理,每一个线程中都会利用paramiko在服务器相应的log目录下tail -f日志获得刷新。

django搭建一个小型的服务器运维网站-基于websocket的实时日志实现_第8张图片

  为了服务器有日志的输出,我们在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所示。

django搭建一个小型的服务器运维网站-基于websocket的实时日志实现_第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实时日志的实现,附带说了下前端怎么把动态响应表格改造成实时日志的滚动效果。希望对有需要的童鞋有帮助。

你可能感兴趣的:(django搭建一个小型的服务器运维网站-基于websocket的实时日志实现)