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服务器,客户端浏览器单独进行链接。

图1 实时日志

Websocket原理

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

图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的,这里的握手只是应用层的关系,传输层已经保证了三次握手和四次挥手,每个客户端都可以主动暂停传输或者关闭传输。

图3 websocket连接示意

实时日志

  服务器一旦产生日志,就会传输给需要接收的客户端滚动显示,这样的逻辑利用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{
        // 建立一个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<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_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;",其目的是怕日志内容过多,超出表格,这里可以实现换行。

<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红框
        },
    });
});

图4

图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<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();
    }
}

图6 日志开关

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

服务器

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

图7 websocket服务端

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

图8 基于websocket的实时日志

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

图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实时日志的实现,附带说了下前端怎么把动态响应表格改造成实时日志的滚动效果。希望对有需要的童鞋有帮助。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
搭建一个博客网站可以分为以下几个步骤: 1. 安装 Django 首先需要安装 Django,可以通过 pip 命令进行安装: ``` pip install django ``` 2. 创建 Django 项目 在命令行中进入要创建项目的目录,然后运行以下命令: ``` django-admin startproject myblog ``` 这会在当前目录下创建一个名为 myblog 的 Django 项目。 3. 创建应用 在 Django 中,可以通过应用来组织代码。一个 Django 项目可以包含多个应用。运行以下命令创建一个名为 blog 的应用: ``` python manage.py startapp blog ``` 这会在 myblog 目录下创建一个名为 blog 的应用。 4. 配置数据库 Django 默认使用 SQLite3 数据库,可以在 myblog/settings.py 文件中进行配置。例如,将数据库改为 MySQL: ```python DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', 'NAME': 'myblog', 'USER': 'mybloguser', 'PASSWORD': 'mypassword', 'HOST': 'localhost', 'PORT': '3306', } } ``` 5. 定义模型 在 blog 应用中,可以定义模型来描述博客文章的结构。打开 blog/models.py 文件,添加以下内容: ```python from django.db import models class Post(models.Model): title = models.CharField(max_length=200) content = models.TextField() pub_date = models.DateTimeField(auto_now_add=True) ``` 6. 创建数据库表 运行以下命令创建数据库表: ``` python manage.py makemigrations python manage.py migrate ``` 7. 创建视图 在 blog/views.py 文件中创建视图函数: ```python from django.shortcuts import render from blog.models import Post def post_list(request): posts = Post.objects.all() return render(request, 'blog/post_list.html', {'posts': posts}) ``` 8. 创建模板 在 blog/templates/blog 目录下创建一个名为 post_list.html 的模板文件,添加以下内容: ```html {% for post in posts %} <h2>{{ post.title }}</h2> <p>{{ post.content }}</p> <p>{{ post.pub_date }}</p> {% endfor %} ``` 9. 配置 URL 在 myblog/urls.py 文件中添加以下内容: ```python from django.urls import path from blog.views import post_list urlpatterns = [ path('', post_list, name='post_list'), ] ``` 10. 运行开发服务器 运行以下命令启动 Django 开发服务器: ``` python manage.py runserver ``` 11. 浏览网站 在浏览器中访问 http://localhost:8000/,可以看到博客文章列表。 以上是 Django 搭建一个博客网站的基本步骤,可以根据实际需要进行扩展和修改。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值