希望通过对这两篇文章的学习,能够对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标识文件的路径

  1. TAILF = {
  2. 1: '/ops/coffee/error.log',
  3. 2: '/ops/coffee/access.log',
  4. }

基础Web页面搭建

假设你已经创建好了一个叫tailf的app,并添加到了settings.py的INSTALLED_APPS中,app的目录结构大概如下

  1. tailf
  2. - migrations
  3. - __init__.py
  4. - __init__.py
  5. - admin.py
  6. - apps.py
  7. - models.py
  8. - tests.py
  9. - views.py

依然先构建一个标准的Django页面,相关代码如下

url:

  1. from django.urls import path
  2. from django.contrib.auth.views import LoginView,LogoutView
  3. from tailf.views import tailf
  4. urlpatterns = [
  5. path('tailf', tailf, name='tailf-url'),
  6. path('login', LoginView.as_view(template_name='login.html'), name='login-url'),
  7. path('logout', LogoutView.as_view(template_name='login.html'), name='logout-url'),
  8. ]

因为我们规定只有通过登录的用户才能查看日志,所以引入Django自带的LoginView,logoutView帮助我们快速构建Login,Logout功能

指定了登录模板使用login.html,它就是一个标准的登录页面,post传入username和password两个参数即可,不贴代码了

view:

  1. from django.conf import settings
  2. from django.shortcuts import render
  3. from django.contrib.auth.decorators import login_required
  4. # Create your views here.
  5. @login_required(login_url='/login')
  6. def tailf(request):
  7. logDict = settings.TAILF
  8. return render(request, 'tailf/index.html', {"logDict": logDict})

引入了login_required装饰器,来判断用户是否登录,未登录就给跳到/login登录页面

logDict 去setting里取我们定义好的TAILF字典赋值,并传递给前端

template:

  1. {% extends "base.html" %}
  2. {% block content %}
  3. <div class="col-sm-8">
  4. <select class="form-control" id="file">
  5. <option value="">选择要监听的日志</option>
  6. {% for k,v in logDict.items %}
  7. <option value="{{ k }}">{{ v }}</option>
  8. {% endfor %}
  9. </select>
  10. </div>
  11. <div class="col-sm-2">
  12. <input class="btn btn-success btn-block" type="button" onclick="connect()" value="开始监听"/><br/>
  13. </div>
  14. <div class="col-sm-2">
  15. <input class="btn btn-warning btn-block" type="button" onclick="goclose()" value="终止监听"/><br/>
  16. </div>
  17. <div class="col-sm-12">
  18. <textarea class="form-control" id="chat-log" disabled rows="20"></textarea>
  19. </div>
  20. {% endblock %}

前端拿到TAILF后通过循环的方式填充到select选择框下,因为数据是字典格式,使用logDict.items的方式可以循环出字典的key和value

这样一个日志监听页面就完成了,但还无法实现日志的监听,继续往下

集成Channels实现WebSocket

日志监听功能主要的设计思路就是页面跟后端服务器建立websocket长连接,后端通过celery异步执行while循环不断的读取日志文件然后发送到websocket的channel里,实现页面上的实时显示

接着我们来集成channels

  1. 先添加routing路由,直接修改webapp/routing.py
  1. from channels.auth import AuthMiddlewareStack
  2. from channels.routing import ProtocolTypeRouter, URLRouter
  3. from django.urls import path, re_path
  4. from chat.consumers import ChatConsumer
  5. from tailf.consumers import TailfConsumer
  6. application = ProtocolTypeRouter({
  7. 'websocket': AuthMiddlewareStack(
  8. URLRouter([
  9. path('ws/chat/', ChatConsumer),
  10. re_path(r'^ws/tailf/(?P<id>\d+)/$', TailfConsumer),
  11. ])
  12. )
  13. })

直接将路由信息写入到了URLRouter里,注意路由信息的外层多了一个list,区别于上一篇中介绍的写路由文件路径的方式

页面需要将监听的日志文件传递给后端,我们使用routing正则P<id>\d+传文件ID给后端程序,后端程序拿到ID之后根据settings中指定的TAILF解析出日志路径

routing的写法跟Django中的url写法完全一致,使用re_path匹配正则routing路由

  1. 添加consumer在tailf/consumers.py文件中
  1. import json
  2. from channels.generic.websocket import WebsocketConsumer
  3. from tailf.tasks import tailf
  4. class TailfConsumer(WebsocketConsumer):
  5. def connect(self):
  6. self.file_id = self.scope["url_route"]["kwargs"]["id"]
  7. self.result = tailf.delay(self.file_id, self.channel_name)
  8. print('connect:', self.channel_name, self.result.id)
  9. self.accept()
  10. def disconnect(self, close_code):
  11. # 中止执行中的Task
  12. self.result.revoke(terminate=True)
  13. print('disconnect:', self.file_id, self.channel_name)
  14. def send_message(self, event):
  15. self.send(text_data=json.dumps({
  16. "message": event["message"]
  17. }))

这里使用Channels的单通道模式,每一个新连接都会启用一个新的channel,彼此互不影响,可以随意终止任何一个监听日志的请求

connect

我们知道self.scope类似于Django中的request,记录了丰富的请求信息,通过self.scope["url_route"]["kwargs"]["id"]取出routing中正则匹配的日志ID

然后将idchannel_name传递给celery的任务函数tailf,tailf根据id取到日志文件的路径,然后循环文件,将新内容根据channel_name写入对应channel

disconnect

当websocket连接断开的时候我们需要终止Celery的Task执行,以清除celery的资源占用

终止Celery任务使用到revoke指令,采用如下代码来实现

  1. self.result.revoke(terminate=True)

注意self.result是一个result对象,而非id

参数terminate=True的意思是是否立即终止Task,为True时无论Task是否正在执行都立即终止,为False(默认)时需要等待Task运行结束之后才会终止,我们使用了While循环不设置为True就永远不会终止了

终止Celery任务的另外一种方法是:

  1. from webapp.celery import app
  2. 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实现代码如下:

  1. from __future__ import absolute_import
  2. from celery import shared_task
  3. import time
  4. from channels.layers import get_channel_layer
  5. from asgiref.sync import async_to_sync
  6. from django.conf import settings
  7. @shared_task
  8. def tailf(id, channel_name):
  9. channel_layer = get_channel_layer()
  10. filename = settings.TAILF[int(id)]
  11. try:
  12. with open(filename) as f:
  13. f.seek(0, 2)
  14. while True:
  15. line = f.readline()
  16. if line:
  17. print(channel_name, line)
  18. async_to_sync(channel_layer.send)(
  19. channel_name,
  20. {
  21. "type": "send.message",
  22. "message": "微信公众号【运维咖啡吧】原创 版权所有 " + str(line)
  23. }
  24. )
  25. else:
  26. time.sleep(0.5)
  27. except Exception as e:
  28. print(e)

这里边主要涉及到Channels中另一个非常重要的点:从Channels的外部发送消息给Channel

其实上篇文章中检查通道层是否能够正常工作的时候使用的方法就是从外部给Channel通道发消息的示例,本文的具体代码如下

  1. async_to_sync(channel_layer.send)(
  2. channel_name,
  3. {
  4. "type": "send.message",
  5. "message": "微信公众号【运维咖啡吧】原创 版权所有 " + str(line)
  6. }
  7. )

channel_name 对应于传递给这个任务的channel_name,发送消息给这个名字的channel

type 对应于我们Channels的TailfConsumer类中的send_message方法,将方法中的_换成.即可

message 就是要发送给这个channel的具体信息

上边是发送给单Channel的情况,如果是需要发送到Group的话需要使用如下代码

  1. async_to_sync(channel_layer.group_send)(
  2. group_name,
  3. {
  4. 'type': 'chat.message',
  5. 'message': '欢迎关注公众号【运维咖啡吧】'
  6. }
  7. )

只需要将发送单channel的send改为group_sendchannel_name改为group_name即可

需要特别注意的是:使用了channel layer之后一定要通过async_to_sync来异步执行

页面添加WebSocket支持

后端功能都已经完成,我们最后需要添加前端页面支持WebSocket

  1. function connect() {
  2. if ( $('#file').val() ) {
  3. window.chatSocket = new WebSocket(
  4. 'ws://' + window.location.host + '/ws/tailf/' + $('#file').val() + '/');
  5. chatSocket.onmessage = function(e) {
  6. var data = JSON.parse(e.data);
  7. var message = data['message'];
  8. document.querySelector('#chat-log').value += (message);
  9. // 跳转到页面底部
  10. $('#chat-log').scrollTop($('#chat-log')[0].scrollHeight);
  11. };
  12. chatSocket.onerror = function(e) {
  13. toastr.error('服务端连接异常!')
  14. };
  15. chatSocket.onclose = function(e) {
  16. toastr.error('websocket已关闭!')
  17. };
  18. } else {
  19. toastr.warning('请选择要监听的日志文件')
  20. }
  21. }

上一篇文章中有详细介绍过websocket的消息类型,这里不多介绍了

至此我们一个日志监听页面完成了,包含了完整的监听功能,但还无法终止,接着看下面的内容

Web页面主动断开WebSocket

web页面上“终止监听”按钮的主要逻辑就是触发WebSocket的onclose方法,从而可以触发Channels后端consumer的disconnect方法,进而终止Celery的循环读取日志任务

前端页面通过.close()可以直接触发WebSocket关闭,当然你如果直接关掉页面的话也会触发WebSocket的onclose消息,所以不用担心Celery任务无法结束的问题

  1. function goclose() {
  2. console.log(window.chatSocket);
  3. window.chatSocket.close();
  4. window.chatSocket.onclose = function(e) {
  5. toastr.success('已终止日志监听!')
  6. };
  7. }

至此我们包含完善功能的Tailf日志监听、终止页面就全部完成了

写在最后

两篇文章结束不知道你是否对Channels有了更深一步的了解,能够操刀上手将Channels用在自己的项目中,实现理想的功能。个人觉得Channels的重点和难点在于对channel layer的理解和运用,真正的理解了并能熟练运用,相信你一定能够举一反三完美实现更多需求。最后如果对本文的demo源码感兴趣可以关注微信公众号【运维咖啡吧】后台回复小二加我微信向我索取,一定有求必应


相关文章推荐阅读:

Django使用Channels实现WebSocket--下篇的更多相关文章

  1. Django使用channels实现Websocket连接

    简述: 需求:消息实时推送消息以及通知功能,采用django-channels来实现websocket进行实时通讯.并使用docker.daphne启动通道,保持websocket后台运行 介绍Dja ...

  2. Django使用Channels实现WebSocket--上篇

    WebSocket - 开启通往新世界的大门 WebSocket是什么? WebSocket是一种在单个TCP连接上进行全双工通讯的协议.WebSocket允许服务端主动向客户端推送数据.在WebSo ...

  3. Django使用channel实现websocket

    channel 什么是channel? channel是第三方工具包,对于不支持websocket协议的框架可以借助此包实现websocket 安装 终端安装: pip3 install channe ...

  4. 6.channels 配置websocket

      Django默认不支持websockey,需要Django支持的话需要安装第三方组件 django channels 是django支持websocket的一个模块.   1.安装 pip3 in ...

  5. Ubuntu + Django(DRF) + channels(websocket)+NGINX + uwsgi 环境部署

    原来uwsgi并不能启动  asgi  呀!现在才知道,就因为这一点我花了一周时间才成功啊!!!!!!!! 是呀!你启动uwsgi 是将你的项目启动了,可是你也发现虽然启动了,但是你的websocke ...

  6. 堡垒机WebSSH进阶之实时监控和强制下线

    这个功能我可以不用,但你不能没有 前几篇文章实现了对物理机.虚拟机以及Kubernetes中Pod的WebSSH操作,可以方便的在web端对系统进行管理,同时也支持对所有操作进行全程录像,以方便后续的 ...

  7. 再聊我们自研的那些Devops工具

    两年前我写了篇文章『我们自研的那些Devops工具』介绍了我们自研的一些DevOps工具系统,两年过去了这些工具究竟还有没有在发光发热,又有哪些新的变化呢,我将通过这篇文章来回顾一下这两年的发展与变化 ...

  8. 实时 Django 终于来了 —— Django Channels 入门指南

    Reference: http://www.oschina.net/translate/in_deep_with_django_channels_the_future_of_real_time_app ...

  9. django + nginx + uwsgi + websocket

    最近使用django框架做了一个简单的聊天机器人demo, 开发的过程中使用了django自带的websocket模块,当使用django框架自带的wsgi服务去启动的话,没有什么问题.如果要使用uw ...

随机推荐

  1. fiddler不能抓某些的包的原因

    用fiddler抓某app的包时,死活抓不到,确定自己设置的没有错,并且让小A同事也看了一遍我的设置,确认没错后,又在小A同事那儿试了下还是抓不到 后来在网上找了很多资料,才发现是因为一些app使用了 ...

  2. 五分钟让你读懂UML常见类图

    相信各位同学在阅读一些源码分析类文章或是设计应用架构时没少与UML类图打交道.实际上,UML类图中最常用到的元素五分钟就能掌握,经常看到UML类图但还不太熟悉的小伙伴赶紧来一起认识一下它吧:)   一 ...

  3. PHP日期格式化函数

    date函数 描述:格式化一个本地时间/日期 语法:string date(string format [,int timestamp]) format字符 说明 format字符 说明 Y 4位数字 ...

  4. Linux 第十五天

    2)环境变量:这种变量中主要保存的是和系统操作环境相关的数据. export 变量名=变量值    设置环境变量 env                     查询变量 unset变量名       ...

  5. c# 将object尝试转为指定对象

    主方法: /// <summary> /// 将object尝试转为指定对象 /// </summary> /// <param name="data" ...

  6. centos 6.5升级openssl

    1.下载升级版本 wget https://www.openssl.org/source/openssl-1.1.0i.tar.gz 2.安装 zlib zlib-devel yum -y insta ...

  7. JS模块化工具require.js教程(二):基本知识

    前一篇:JS模块化工具我们以非常简单的方式引入了requirejs,这一篇将讲述一下requirejs中的一些基本知识,包括API使用方式等 基本API require会定义三个变量:define,r ...

  8. 修改MariaDB 路径

    1.把mariadb服务停掉: service mariadb stop 2.把/var/lib/mysql整个目录复制到/work, sudo mkdir /work/data sudo cp -a ...

  9. python property对象

    一.从@porperty说起 Python内置的@property装饰器是负责把一个方法变成属性调用的 class Stu(object): def __init__(self,age): self. ...

  10. Shell文本操作-5