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 ,内容如下:
将从一个空路由配置开始。在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
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