Django3+websocket+paramiko实现远程linux shell 执行内容实时输出到web页面

一.Python中paramiko连接远程linux服务器

    python中paramiko库可以执行远程linux服务器命令并接收返回结果,基于channel信道模式,channel允许用户在不同程序间进行通信,这是实现分布式实时应用的一部分如果你不想所有的message和event都经由数据库的话
此外,它还可以和工作进程结合使用来创建基本的任务队列或者卸载任务
但是channel本身是不附带任何开箱即用的channel layer的,因为每一个channel layer都依赖于不同的网络数据传输方式
channel官方推荐的是配置channel_redis,这是一个使用Redis作为传输的Django维护层
channel2.0不同于1.x,channel layer属于完全可选部分,这意味着如果不想使用的话可以设置成空字典{}或者CHANNEL_LAYER不配置即可
同时,channel layer属于纯粹的异步接口,如果想要从同步代码调用,需要使用装饰器asgiref.sync.async_to_sync
其中channel配置redis方法如下:

CHANNEL_LAYERS = {
    
    
    "default": {
    
    
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
    
    
            "hosts": [("redis-server-name", 6379)],
        },
    },
}

默认获取channel_layer的方式是调用接口:channels.layers.get_channel_layer(),如果是在consumer中调用接口的话可以直接使用self.channel_layer
同步函数
对于channel layer的方法(包括send()、group_send(),group_add()等)都属于异步方法,这意味着在调用的时候都需要使用await,而如果想要在同步代码中使用它们,就需要使用装饰器asgiref.sync.async_to_sync

from asgiref.sync import async_to_sync

async_to_sync(channel_layer.send)("channel_name", {
    
    ...})

二.Django3实现远端linux执行结果实时输出到web

整体项目目录结构如下
在这里插入图片描述

2.1安装paramiko,channel模块
pip3 install paramiko==2.7.2
pip install -U channels

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

INSTALLED_APPS = [
    # ...    'channels',  # 【channels】(第1步)pip install -U channels 安装
    # ...
]
2.2 创建默认路由(主websocket路由)

为了让websocket的路由和django中urls的路由系统区分,我们在web_info_api目录下新建一个route.py模块来实现websocket的路由(其实也可以合入到urls.py,这里只是进行区分),内容如下:

from django.urls import path
from . import views

websocket_urlpatterns = [
    # 前端请求websocket连接
    path('ws/result/', views.SyncConsumer),
]
2.3 应用(application)转发配置(基于asgi)

通常django应用application都是基于wsgi协议的,但是为了django为了兼容原有的wsgi以及支持websocket,衍生出asgi这一拓展,在与wsgi.py的同级目录下新建routing.py模块,用来转发基于asgi的websocket的application请求处理

设置执行路由对象(指定routing)
最后,将ASGI_APPLICATION设置为指向路由对象作为根应用程序,修改 settings.py 文件,最后一行添加:
ASGI_APPLICATION = ‘websocket_project.routing.application’
就是这样!一旦启用,通道就会将自己集成到Django中,并控制runserver命令。

2.4 启用信道层channel

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

这里为了方便部署,直接使用内存作为后备存储的通道层。有条件的话,可以使用redis存储
配置CHANNEL_LAYERS
修改 settings.py 最后一行增加配置

CHANNEL_LAYERS = {
    
    
    "default": {
    
    
        "BACKEND": "channels.layers.InMemoryChannelLayer",
    }
}
2.5 实现Channel消费者端业务逻辑(集成到视图view中)

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

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

from django.shortcuts import render
import json
import paramiko
import logging
from channels.generic.websocket import AsyncWebsocketConsumer
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
# Create your views here.


def index(request):
    return render(request, 'index.html')


class SyncConsumer(WebsocketConsumer):

    def __init__(self, *args, **kwargs):
        super(SyncConsumer, self).__init__(*args, **kwargs)
        self.channel_group_name = None
        self.username = None

    def connect(self):
        self.username = 'mym'
        logging.info('WebSocket建立连接:%s' % self.username)
        # 直接从用户指定的通道名称构造通道组名称
        self.channel_group_name = 'msg_%s' % self.username
        print(self.channel_name)
        # 加入通道层
        # async_to_sync(…)包装器是必需的,因为ChatConsumer是同步WebsocketConsumer,但它调用的是异步通道层方法。(所有通道层方法都是异步的。)
        async_to_sync(self.channel_layer.group_add)(
            self.channel_group_name,
            self.channel_name,
        )

        # 接受WebSocket连接。
        self.accept()

    def disconnect(self, close_code):
        logging.info('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 = '127.0.0.1'
                username = 'xxxx'
                password = 'xxxxx'

                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连接
                print(self.channel_group_name)
                self.disconnect(self.channel_group_name)
                print("后端关闭websocket连接")
2.6 Web前端中实现websocket连接

html5中js中有处理websocket的api,直接实例化websocket对象即可,前端代码如下:
django中放到templates目录下,新建index.html文件

<!DOCTYPE html>
<html lang="en">
<head>
    <meta content="text/html; charset=UTF-8"/>
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>bash输出</title>
    <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/[email protected]/dist/css/bootstrap.min.css">
    <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:#000;color:#FFF;">
        </div>
    </div>
</div>

</body>
    <script type="text/javascript">
        //点击按钮触发
        $('#execute_script').click(function() {
     
     
            // 与远程服务器建立websocket连接
            console.log(window.location.host);
            const chatSocket = new WebSocket(
                'ws://' + window.location.host + '/ws/result/'
            );

            // 连接建立成功事件
            chatSocket.onopen = function() {
     
     
                console.log('WebSocket open');
                // 发送字符串:build the channel 到服务端
                chatSocket.send(JSON.stringify({
     
     
                    "message": 'laying_eggs'
                }));
                console.log('发送laying_eggs完成')
            };

            // 接收消息事件
            chatSocket.onmessage = function(e) {
     
     
                // 打印服务端返回的数据
                console.log('message:' + e.data)
                // 转换为字符串, 防止卡死显示窗口
                $('#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>
2.7 配置django路由,编写视图渲染html

修改urls.py,增加首页

from django.conf.urls import url
from django.contrib import admin
from websocket_info_api import views

urlpatterns = [
    url(r'^admin/', admin.site.urls),
    url(r'index/', views.index),

]

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

def index(request):
    return render(request, 'index.html')

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

python manage.py runserver 0.0.0.0:8000

注意点:如果想看WebsocketConsumer底层实现的一些创建信道名称,信道组名称,创建连接,发送消息等方法,可以查看源码,一般在处理websocket请求时在消费者业务逻辑代码中需要进行方法重构,添加实际业务逻辑

猜你喜欢

转载自blog.csdn.net/qq_42707967/article/details/114255500