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

发布时间:2020-10-13 14:40:56编辑:admin阅读(4373)

    一、概述

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

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

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

    1.gif

     

    二、代码实现

    环境说明

    操作系统: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

    1.png

     

     

     

    安装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

     


关键字

上一篇: django3 websockets

下一篇: 没有了