Django使用Channels实现WebSocket--下篇
希望通过对这两篇文章的学习,能够对Channels有更加深入的了解,使用起来得心应手游刃有余
通过上一篇《Django使用Channels实现WebSocket--上篇》的学习应该对Channels的各种概念有了清晰的认知,可以顺利的将Channels框架集成到自己的Django项目中实现WebSocket了,本篇文章将以一个Channels+Celery实现web端tailf功能的例子更加深入的介绍Channels
先说下我们要实现的目标:所有登录的用户可以查看tailf日志页面,在页面上能够选择日志文件进行监听,多个页面终端同时监听任何日志都互不影响,页面同时提供终止监听的按钮能够终止前端的输出以及后台对日志文件的读取
最终实现的结果见下图
接着我们来看下具体的实现过程
技术实现
所有代码均基于以下软件版本:
- python==3.6.3
- django==2.2
- channels==2.1.7
- celery==4.3.0
celery4在windows下支持不完善,所以请在linux下运行测试
日志数据定义
我们只希望用户能够查询固定的几个日志文件,就不是用数据库仅借助settings.py文件里写全局变量来实现数据存储
在settings.py里添加一个叫TAILF
的变量,类型为字典,key标识文件的编号,value标识文件的路径
TAILF = { 1: '/ops/coffee/error.log', 2: '/ops/coffee/access.log', }
基础Web页面搭建
假设你已经创建好了一个叫tailf的app,并添加到了settings.py的INSTALLED_APPS中,app的目录结构大概如下
tailf - migrations - __init__.py - __init__.py - admin.py - apps.py - models.py - tests.py - views.py
依然先构建一个标准的Django页面,相关代码如下
url:
from django.urls import path from django.contrib.auth.views import LoginView,LogoutView from tailf.views import tailf urlpatterns = [ path('tailf', tailf, name='tailf-url'), path('login', LoginView.as_view(template_name='login.html'), name='login-url'), path('logout', LogoutView.as_view(template_name='login.html'), name='logout-url'), ]
因为我们规定只有通过登录的用户才能查看日志,所以引入Django自带的LoginView,logoutView帮助我们快速构建Login,Logout功能
指定了登录模板使用login.html
,它就是一个标准的登录页面,post传入username和password两个参数即可,不贴代码了
view:
from django.conf import settings from django.shortcuts import render from django.contrib.auth.decorators import login_required # Create your views here. @login_required(login_url='/login') def tailf(request): logDict = settings.TAILF return render(request, 'tailf/index.html', {"logDict": logDict})
引入了login_required
装饰器,来判断用户是否登录,未登录就给跳到/login
登录页面
logDict 去setting里取我们定义好的TAILF
字典赋值,并传递给前端
template:
{% extends "base.html" %} {% block content %} <div class="col-sm-8"> <select class="form-control" id="file"> <option value="">选择要监听的日志</option> {% for k,v in logDict.items %} <option value="{{ k }}">{{ v }}</option> {% endfor %} </select> </div> <div class="col-sm-2"> <input class="btn btn-success btn-block" type="button" onclick="connect()" value="开始监听"/><br/> </div> <div class="col-sm-2"> <input class="btn btn-warning btn-block" type="button" onclick="goclose()" value="终止监听"/><br/> </div> <div class="col-sm-12"> <textarea class="form-control" id="chat-log" disabled rows="20"></textarea> </div> {% endblock %}
前端拿到TAILF
后通过循环的方式填充到select选择框下,因为数据是字典格式,使用logDict.items
的方式可以循环出字典的key和value
这样一个日志监听页面就完成了,但还无法实现日志的监听,继续往下
集成Channels实现WebSocket
日志监听功能主要的设计思路就是页面跟后端服务器建立websocket长连接,后端通过celery异步执行while循环不断的读取日志文件然后发送到websocket的channel里,实现页面上的实时显示
接着我们来集成channels
- 先添加routing路由,直接修改
webapp/routing.py
from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter from django.urls import path, re_path from chat.consumers import ChatConsumer from tailf.consumers import TailfConsumer application = ProtocolTypeRouter({ 'websocket': AuthMiddlewareStack( URLRouter([ path('ws/chat/', ChatConsumer), re_path(r'^ws/tailf/(?P<id>\d+)/$', TailfConsumer), ]) ) })
直接将路由信息写入到了URLRouter
里,注意路由信息的外层多了一个list,区别于上一篇中介绍的写路由文件路径的方式
页面需要将监听的日志文件传递给后端,我们使用routing正则P<id>\d+
传文件ID给后端程序,后端程序拿到ID之后根据settings中指定的TAILF
解析出日志路径
routing的写法跟Django中的url写法完全一致,使用re_path
匹配正则routing路由
- 添加consumer在
tailf/consumers.py
文件中
import json from channels.generic.websocket import WebsocketConsumer from tailf.tasks import tailf class TailfConsumer(WebsocketConsumer): def connect(self): self.file_id = self.scope["url_route"]["kwargs"]["id"] self.result = tailf.delay(self.file_id, self.channel_name) print('connect:', self.channel_name, self.result.id) self.accept() def disconnect(self, close_code): # 中止执行中的Task self.result.revoke(terminate=True) print('disconnect:', self.file_id, self.channel_name) def send_message(self, event): self.send(text_data=json.dumps({ "message": event["message"] }))
这里使用Channels的单通道模式,每一个新连接都会启用一个新的channel,彼此互不影响,可以随意终止任何一个监听日志的请求
connect
我们知道self.scope
类似于Django中的request,记录了丰富的请求信息,通过self.scope["url_route"]["kwargs"]["id"]
取出routing中正则匹配的日志ID
然后将id
和channel_name
传递给celery的任务函数tailf,tailf根据id
取到日志文件的路径,然后循环文件,将新内容根据channel_name
写入对应channel
disconnect
当websocket连接断开的时候我们需要终止Celery的Task执行,以清除celery的资源占用
终止Celery任务使用到revoke
指令,采用如下代码来实现
self.result.revoke(terminate=True)
注意self.result
是一个result对象,而非id
参数terminate=True
的意思是是否立即终止Task,为True时无论Task是否正在执行都立即终止,为False(默认)时需要等待Task运行结束之后才会终止,我们使用了While循环不设置为True就永远不会终止了
终止Celery任务的另外一种方法是:
from webapp.celery import app app.control.revoke(result.id, terminate=True)
send_message
方便我们通过Django的view或者Celery的task调用给channel发送消息,官方也比较推荐这种方式
使用Celery异步循环读取日志
上边已经集成了Channels实现了WebSocket,但connect函数中的celery任务tailf
还没有实现,下边来实现它
关于Celery的详细内容可以看这篇文章:《Django配置Celery执行异步任务和定时任务》,本文就不介绍集成使用以及细节原理,只讲一下任务task
task实现代码如下:
from __future__ import absolute_import from celery import shared_task import time from channels.layers import get_channel_layer from asgiref.sync import async_to_sync from django.conf import settings @shared_task def tailf(id, channel_name): channel_layer = get_channel_layer() filename = settings.TAILF[int(id)] try: with open(filename) as f: f.seek(0, 2) while True: line = f.readline() if line: print(channel_name, line) async_to_sync(channel_layer.send)( channel_name, { "type": "send.message", "message": "微信公众号【运维咖啡吧】原创 版权所有 " + str(line) } ) else: time.sleep(0.5) except Exception as e: print(e)
这里边主要涉及到Channels中另一个非常重要的点:从Channels的外部发送消息给Channel
其实上篇文章中检查通道层是否能够正常工作的时候使用的方法就是从外部给Channel通道发消息的示例,本文的具体代码如下
async_to_sync(channel_layer.send)( channel_name, { "type": "send.message", "message": "微信公众号【运维咖啡吧】原创 版权所有 " + str(line) } )
channel_name 对应于传递给这个任务的channel_name,发送消息给这个名字的channel
type 对应于我们Channels的TailfConsumer类中的send_message
方法,将方法中的_
换成.
即可
message 就是要发送给这个channel的具体信息
上边是发送给单Channel的情况,如果是需要发送到Group的话需要使用如下代码
async_to_sync(channel_layer.group_send)( group_name, { 'type': 'chat.message', 'message': '欢迎关注公众号【运维咖啡吧】' } )
只需要将发送单channel的send
改为group_send
,channel_name
改为group_name
即可
需要特别注意的是:使用了channel layer之后一定要通过async_to_sync来异步执行
页面添加WebSocket支持
后端功能都已经完成,我们最后需要添加前端页面支持WebSocket
function connect() { if ( $('#file').val() ) { window.chatSocket = new WebSocket( 'ws://' + window.location.host + '/ws/tailf/' + $('#file').val() + '/'); chatSocket.onmessage = function(e) { var data = JSON.parse(e.data); var message = data['message']; document.querySelector('#chat-log').value += (message); // 跳转到页面底部 $('#chat-log').scrollTop($('#chat-log')[0].scrollHeight); }; chatSocket.onerror = function(e) { toastr.error('服务端连接异常!') }; chatSocket.onclose = function(e) { toastr.error('websocket已关闭!') }; } else { toastr.warning('请选择要监听的日志文件') } }
上一篇文章中有详细介绍过websocket的消息类型,这里不多介绍了
至此我们一个日志监听页面完成了,包含了完整的监听功能,但还无法终止,接着看下面的内容
Web页面主动断开WebSocket
web页面上“终止监听”按钮的主要逻辑就是触发WebSocket的onclose方法,从而可以触发Channels后端consumer的disconnect
方法,进而终止Celery的循环读取日志任务
前端页面通过.close()
可以直接触发WebSocket关闭,当然你如果直接关掉页面的话也会触发WebSocket的onclose消息,所以不用担心Celery任务无法结束的问题
function goclose() { console.log(window.chatSocket); window.chatSocket.close(); window.chatSocket.onclose = function(e) { toastr.success('已终止日志监听!') }; }
至此我们包含完善功能的Tailf日志监听、终止页面就全部完成了
写在最后
两篇文章结束不知道你是否对Channels有了更深一步的了解,能够操刀上手将Channels用在自己的项目中,实现理想的功能。个人觉得Channels的重点和难点在于对channel layer的理解和运用,真正的理解了并能熟练运用,相信你一定能够举一反三完美实现更多需求。最后如果对本文的demo源码感兴趣可以关注微信公众号【运维咖啡吧】后台回复小二加我微信向我索取,一定有求必应
相关文章推荐阅读:
低调大师中文资讯倾力打造互联网数据资讯、行业资源、电子商务、移动互联网、网络营销平台。
持续更新报道IT业界、互联网、市场资讯、驱动更新,是最及时权威的产业资讯及硬件资讯报道平台。
转载内容版权归作者及来源网站所有,本站原创内容转载请注明来源。
- 上一篇
结构型模式:桥接模式
文章首发: 结构型模式:桥接模式 七大结构型模式之二:桥接模式。 简介 姓名 :桥接模式 英文名 :Bridge Pattern 价值观 :解耦靠我 个人介绍 : Decouple an abstraction from its implementation so that the two can vary independently. 将抽象和实现解耦,使得两者可以独立地变化。 (来自《设计模式之禅》) 你要的故事 现在手机二分天下,安卓手机和苹果手机目前占有率高居 98.45%,其中安卓手机占有率为 70.21%,苹果手机占有率为 28.24%,如下图所示。 (数据从 netmarketshare 来) 因为有这 2 个系统,所以很多软件商都不得不开发 2 个系统的 APP。我们就拿这个案例来讲,目前手机有安卓手机和苹果手机,软件有谷歌浏览器和火狐浏览器,通过手机打开软件这一过程来讲讲桥接模式。 从个人介绍可见,需要抽象化和实现化,然后使用桥接模式将抽象和实现解耦。 抽象化:把一类对象共有的东西抽象到一个类里面,该类作为这类对象的基类。在这里我们可以抽象化的便是手机。 实现化:将接...
- 下一篇
Java日志正确使用姿势
前言 关于日志,在大家的印象中都是比较简单的,只须引入了相关依赖包,剩下的事情就是在项目中“尽情”的打印我们需要的信息了。但是往往越简单的东西越容易让我们忽视,从而导致一些不该有的bug发生,作为一名严谨的程序员,怎么能让这种事情发生呢?所以下面我们就来了解一下关于日志的那些正确使用姿势。 正文 日志规范 命名 首先是日志文件的命名,尽量要做到见名知意,团队里面也必须使用统一的命名规范,不然“脏乱差”的日志文件会影响大家排查问题的效率。这里推荐以“projectName_logName_logType.log”来命名,这样通过名字就可以清晰的知道该日志文件是属于哪个项目,什么类型,有什么作用。例如在我们MessageServer项目中监控Rabbitmq 消费者相关的日志文件名可以定义成“messageserver_rabbitmqconsumer_monitor.log”。 保存时间 关于日志保存的时间,普通的日志文件建议保留15天,若比较重要的可根据实际情况延长,具体请参考各自服务器磁盘空间以及日志文件大小作出最优选择。 日志级别 常见的日志级别有以下: DEBUG级别:记录调试程...
相关文章
文章评论
共有0条评论来说两句吧...
文章二维码
点击排行
推荐阅读
最新文章
- CentOS关闭SELinux安全模块
- CentOS7编译安装Gcc9.2.0,解决mysql等软件编译问题
- Windows10,CentOS7,CentOS8安装Nodejs环境
- CentOS7编译安装Cmake3.16.3,解决mysql等软件编译问题
- CentOS6,7,8上安装Nginx,支持https2.0的开启
- CentOS8,CentOS7,CentOS6编译安装Redis5.0.7
- CentOS8安装MyCat,轻松搞定数据库的读写分离、垂直分库、水平分库
- SpringBoot2编写第一个Controller,响应你的http请求并返回结果
- SpringBoot2整合Redis,开启缓存,提高访问速度
- SpringBoot2更换Tomcat为Jetty,小型站点的福音