Django3+websocket+paramiko实现web页面实时输出

时间:2022-07-26
本文章向大家介绍Django3+websocket+paramiko实现web页面实时输出,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

一、概述

在上一篇文章中,简单在浏览器测试了websocket,链接如下:https://www.cnblogs.com/xiao987334176/p/13615170.html

但是,我们最终的效果是web页面上,能够实时输出结果,比如执行一个shell脚本。

以母鸡下蛋的例子,来演示一下,先来看效果:

二、代码实现

环境说明

操作系统:windows 10

python版本:3.7.9

操作系统:centos 7.6

ip地址:192.168.31.196

说明:windows10用来运行django项目,centos系统用来执行shell脚本。脚本路径为:/opt/test.sh,内容如下:

#!/bin/bash

for i in {1..10}
do
    sleep 0.1
    echo 母鸡生了$i个鸡蛋;
done

新建项目

新建项目:django3_websocket,应用名称:web

安装paramiko模块

pip3 install paramiko

编辑 settings.py

将Channels库添加到已安装的应用程序列表中。编辑 settings.py 文件,并将channels添加到INSTALLED_APPS设置中。

INSTALLED_APPS = [
    # ...    'channels',  # 【channels】(第1步)pip install -U channels 安装
    # ...
]

创建默认路由(主WS路由)

Channels路由配置类似于Django URLconf,因为当通道服务器接收到HTTP请求时,它告诉通道运行什么代码。 将从一个空路由配置开始。在web目录下,创建一个文件 routing.py ,内容如下:

from django.urls import re_path,path

from . import consumers

websocket_urlpatterns = [
    # 前端请求websocket连接
    path('ws/result/', consumers.SyncConsumer),
]

设置执行路由对象(指定routing)

最后,将ASGI_APPLICATION设置为指向路由对象作为根应用程序,修改 settings.py 文件,最后一行添加:

ASGI_APPLICATION = 'django3_websocket.routing.application'

就是这样!一旦启用,通道就会将自己集成到Django中,并控制runserver命令。

启动channel layer

信道层是一种通信系统。它允许多个消费者实例彼此交谈,以及与Django的其他部分交谈。 通道层提供以下抽象: 通道是一个可以将邮件发送到的邮箱。每个频道都有一个名称。任何拥有频道名称的人都可以向频道发送消息。 一组是一组相关的通道。一个组有一个名称。任何具有组名称的人都可以按名称向组添加/删除频道,并向组中的所有频道发送消息。无法枚举特定组中的通道。 每个使用者实例都有一个自动生成的唯一通道名,因此可以通过通道层进行通信。

这里为了方便部署,直接使用内存作为后备存储的通道层。有条件的话,可以使用redis存储。

配置CHANNEL_LAYERS

修改 settings.py 最后一行增加配置

CHANNEL_LAYERS = {
     "default": {
         "BACKEND": "channels.layers.InMemoryChannelLayer",
     }
}

应用下创建 consumers.py(类似Django视图)

同步消费者很方便,因为他们可以调用常规的同步I / O函数,例如那些在不编写特殊代码的情况下访问Django模型的函数。 但是,异步使用者可以提供更高级别的性能,因为他们在处理请求时不需要创建其他线程。

这里使用同步消费,因为我测试异步消费时,web页面并不能实时展示结果。只能使用同步模式才行。

在web目录下,创建文件consumers.py

import json
from channels.generic.websocket import AsyncWebsocketConsumer
import paramiko
from channels.generic.websocket import WebsocketConsumer, AsyncWebsocketConsumer
from asgiref.sync import async_to_sync

# 同步方式,仅作示例,不使用
class SyncConsumer(WebsocketConsumer):
    def connect(self):
        self.username = "xiao"  # 临时固定用户名
        print('WebSocket建立连接:', self.username)
        # 直接从用户指定的通道名称构造通道组名称
        self.channel_group_name = 'msg_%s' % self.username

        # 加入通道层
        # async_to_sync(…)包装器是必需的,因为ChatConsumer是同步WebsocketConsumer,但它调用的是异步通道层方法。(所有通道层方法都是异步的。)
        async_to_sync(self.channel_layer.group_add)(
            self.channel_group_name,
            self.channel_name
        )  
        
        # 接受WebSocket连接。
        self.accept()

        async_to_sync(self.channel_layer.group_send)(
            self.channel_group_name,
            {
                'type': 'get_message',
            }
        )

    def disconnect(self, close_code):
        print('WebSocket关闭连接')
        # 离开通道
        async_to_sync(self.channel_layer.group_discard)(
            self.channel_group_name,
            self.channel_name
        )

    # 从WebSocket中接收消息
    def receive(self, text_data=None, bytes_data=None):
        print('WebSocket接收消息:', text_data,type(text_data))
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        # print("receive message",message,type(message))
        # 发送消息到通道
        async_to_sync(self.channel_layer.group_send)(
            self.channel_group_name,
            {
                'type': 'get_message',
                'message': message
            }
        )

    # 从通道中接收消息
    def get_message(self, event):
        # print("event",event,type(event))
        if event.get('message'):
            message = event['message']
            # 判断消息
            if message == "close":
                # 关闭websocket连接
                self.disconnect(self.channel_group_name)
                print("前端关闭websocket连接")

            # 判断消息,执行脚本
            if message == "laying_eggs":
                # 执行的命令或者脚本
                command = 'bash /opt/test.sh'

                # 远程连接服务器
                hostname = '192.168.31.196'
                username = 'root'
                password = 'root'

                ssh = paramiko.SSHClient()
                ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
                ssh.connect(hostname=hostname, username=username, password=password)
                # 务必要加上get_pty=True,否则执行命令会没有权限
                stdin, stdout, stderr = ssh.exec_command(command, get_pty=True)
                # result = stdout.read()
                # 循环发送消息给前端页面
                while True:
                    nextline = stdout.readline().strip()  # 读取脚本输出内容
                    # print(nextline.strip())

                    # 发送消息到客户端
                    self.send(
                        text_data=nextline
                    )
                    print("已发送消息:%s" % nextline)
                    # 判断消息为空时,退出循环
                    if not nextline:
                        break

                ssh.close()  # 关闭ssh连接
                # 关闭websocket连接
                self.disconnect(self.channel_group_name)
                print("后端关闭websocket连接")

注意:修改里面的服务器,用户名,密码,脚本名称。

应用下创建 routing.py (类似Django路由)

在web目录下,创建文件routing.py

添加Channels子路由的配置

from django.urls import re_path,path

from . import consumers

websocket_urlpatterns = [
    # 前端请求websocket连接
    path('ws/result/', consumers.SyncConsumer),
]

前端页面连接WebSocket

在templates目录下,新建文件index.html,内容如下:

<!DOCTYPE html >
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title>测试demo</title>
    <!-- 最新版本的 Bootstrap 核心 CSS 文件 -->
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@3.3.7/dist/css/bootstrap.min.css"
          integrity="sha384-BVYiiSIFeK1dGmJRAkycuHAHRg32OmUcww7on3RYdg4Va+PmSTsz/K68vbdEjh4u" crossorigin="anonymous">
    <script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.1.1/jquery.min.js"></script>
</head>
<body>

<div class="container">
    <div style="height: 30px"></div>
    <button type="button" id="execute_script" class="btn btn-success">查看日志</button>

    <h4>日志内容:</h4>
    <div style="height: 600px;overflow: auto;" id="content_logs">
        <div id="messagecontainer" style="font-size: 16px;background-color: black;color: white">
        </div>
    </div>
</div>
</body>
<script type="text/javascript">
    // 点击按钮
    $('#execute_script').click(function () {
        // 新建websocket连接
        const chatSocket = new WebSocket(
            'ws://'
            + window.location.host
            + '/ws/result/'
        );

        // 连接建立成功事件
        chatSocket.onopen = function () {
            console.log('WebSocket open');
            //发送字符: laying_eggs到服务端
            chatSocket.send(JSON.stringify({
                'message': 'laying_eggs'
            }));
            console.log("发送完字符串laying_eggs");
        };
        // 接收消息事件
        chatSocket.onmessage = function (e) {
            {#if (e.data.length > 0) {#}
            //打印服务端返回的数据
            console.log('message: ' + e.data);
            // 转换为字符串,防止卡死testestt
            $('#messagecontainer').append(String(e.data) + '<br/>');
            //滚动条自动到最底部
            $("#content_logs").scrollTop($("#content_logs")[0].scrollHeight);
            {# }#}
        };
        // 关闭连接事件
        chatSocket.onclose = function (e) {
            console.log("connection closed (" + e.code + ")");
            chatSocket.send(JSON.stringify({
                'message': 'close'
            }));
        }
    });
</script>
</html>

修改urls.py,增加首页

from django.contrib import admin
from django.urls import path
from web import views

urlpatterns = [
    path('admin/', admin.site.urls),
    path('index/', views.index),
]

修改web目录下的views.py,内容如下:

from django.shortcuts import render

# Create your views here.
def index(request):
    return render(request,'index.html')

使用Pycharm直接启动项目,或者使用命令行启动

python manage.py runserver

访问首页

http://127.0.0.1:8000/index/

 点击查看日志,效果就是文章开头部分的动态效果了。

完整代码在github中,地址:

https://github.com/py3study/django3_websocket

本文参考链接:

https://www.jianshu.com/p/0f75e2623418