django中channel模块之websocket

https://mp.weixin.qq.com/s/hqaPrPS7w3D-9SeegQAB2Q
本人的demo路径
https://github.com/aawuliliaa/python-test/tree/master/WebSocket

1.WebSocket是什么?

1.1首先讲解下全双工--半双工--单工

全双工:
通信允许数据在两个方向上同时传输。
全双工可以同时进行信号的双向传输,即A-->B的同时B-->A,是瞬间同步的。
日常生活中的电话聊天就是全双工。

半双工:
可分时进行信号的双向传输。
指A-->B时,不能B-->A。
数据可以双向传输,但双向传输不是同时进行的。
例如对讲机,一方讲话的同时,另一方不能讲话。

单工:
单向的数据传输。
只允许A-->B传送消息,不能B-->A传送。
例如日常生活中的广播。

1.2WebSocket协议

WebSocket是在单个TCP连接上进行的全双工通信的协议。
在WebSocket协议中,没有Request和Response的概念,连接一旦建立,就建立了持久连接,双方可以随时向对方发送数据。

1.3WebSocket作用

HTTP协议是浏览器客户端发出请求,服务端进行响应,服务端不能主动发送信息到客户端。
WebSocket协议区别于HTTP协议的最大特点,即WebSocket可以由服务端主动发送消息到浏览器端。
最普遍的应用就是tail -f查看服务器端的日志,可实现动态刷新最新日志。
WebSocket的另外一个应用场景就是聊天室,一个浏览器端发送的消息,其他浏览器端可同时接受。这在HTTP协议下很难实现的。

2.channels

Django本身不支持WebSocket协议,但可以通过Channels框架来实现WebSocket。
下面示例代码的基本环境是
python==3.6.3
django==2.2.3
channels==2.2.0

2.1集成Channels

2.1.1安装channels

pip3 install channels==2.2.0

2.1.2settings.py

INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'testchannel',
    'django_celery_beat',
    'django_celery_results',
    'channels',
]
# 指定ASGI的路由地址
ASGI_APPLICATION = 'untitled.routing.application'
channels运行与ASGI协议上,ASGI的全名是Asynchronous Server Gateway Iterface(异步服务网关接口)。
ASGI是区别于Django使用的WSGI协议的一种异步网关接口协议,正是因为它才实现了websocket。

ASGI_APPLICATION指定路由的位置是project name下的routing.py文件中的application。

2.1.3routing.py

routing.py类似于Django中的url.py,指明websocket协议的路由。

from channels.routing import ProtocolTypeRouter

application = ProtocolTypeRouter({
    # 暂时为空,下文填充
})

2.1.4运行Django项目

ssh://[email protected]:22/usr/bin/python3 -u /project/untitled/manage.py runserver 10.0.0.61:8000
Watching for file changes with StatReloader
Performing system checks...

System check identified no issues (0 silenced).
July 31, 2019 - 12:49:23
Django version 2.2.3, using settings 'untitled.settings'
Starting ASGI/Channels version 2.2.0 development server at http://10.0.0.61:8000/
Quit the server with CONTROL-C.
Django启动中的Starting development server 变成了Starting ASGI/Channels version 2.2.0 development server at
说明项目已经由django使用的WSGI协议变为了Channels使用的ASGI协议。

到这里Django已经集成了Channels框架。

2.2构建聊天室

上面只是集成了channels,但还没有使用它。
下面我们构建一个聊天的例子

2.2.1url.py

from django.contrib import admin
from django.urls import path
from testchannel.views import *
urlpatterns = [
    path('admin/', admin.site.urls),
    path('chat/', chat, name="chat-url"),

]

2.2.2views.py

from django.shortcuts import render

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

2.2.3routing.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from channels.routing import ProtocolTypeRouter,URLRouter
from channels.auth import AuthMiddlewareStack
from django.urls import path,re_path
from untitled.consumers import *

websocket_urlpatterns = [
    path(r"ws/chat/", ChatConsumer),

]

application = ProtocolTypeRouter({
    'websocket':AuthMiddlewareStack(
        URLRouter(
            websocket_urlpatterns
        )
    )
})
ProtocolTypeRouter:
ASGI支持多种不同的协议,在这里可以指定特定协议的路由信息,由于我们只使用了websocket协议,这里只配置了websocket。

AuthMiddlewareStack:
django的channels封装了django的auth模块,使用这个配置就可以在consumer中通过下面的方式获取用户的信息。
self.scope类似于django的request,包含了请求的type,path,header,cookie,session,user等有用的信息。
def connect(self):
    self.user = self.scope["user"]

 URLRouter:
 指定路由文件的路径,或者也可以将路由信息写在这里。
routing.py和django的url.py功能类似,语法也一样,"ws/chat/"是访问路径,ChatConsumer是该路径对应的类。

2.2.4consumer.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from channels.generic.websocket import WebsocketConsumer
import json

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        # 连接时触发
        self.accept()
    def disconnect(self, code):
        # 关闭连接时触发
        pass
    def receive(self, text_data=None, bytes_data=None):
        # 收到消息后触发
                # 前端页面使用send()发送数据给websocket,由该函数处理
        # 真个ChatConsumer类会将所有接收到的消息加上一个"聊天"的前缀发送给客户端
        text_data_json = json.loads(text_data)
        message = "聊天:"+text_data_json["message"]
        self.send(text_data=json.dumps({"message":message}))

2.2.5index.html




    
    Title
    
    





{# 加载bootstrap的js#}
WebSocket对象支持四个消息:onopen,onmessage,onclose和onerror。
onopen:当浏览器和websocket服务端连接成功后触发onopen消息。
onerror:如果连接失败,或发送、接收数据失败,或者数据处理出错,都会触发onerror消息。
onmessage:当浏览器收到websocket服务器发送过来的数据时,就会触发onmessage,参数e.data是服务端发送过来的数据。
onclose:当浏览器接收到websocket服务器发送过来的关闭连接请求时,会触发onclose。

2.3启用channel Layer

上面的例子只是实现了消息的发送和接收,如果另外打开一个浏览器,是看不到消息的。
但如果是聊天室的场景,需要一个人发送消息,多个人的浏览器端都能接收到消息。

Channel引入了一个layer的概念,channel layer是一种通信系统,允许多个consumer实例之间互相通信,以及与外部Django程序实现互通。

Channel layer主要实现了两种概念抽象:
channel name:
channel实际上是一个发送消息的通道,每个channel都有一个名称,每一个拥有这个名称的人都可以往channel里面发送消息

group:
多个channel可以组成一个group,每个group都有一个名称。
每个拥有这个名称的人都可以往这个group里添加/删除channel,也可以往group里发送消息。
group内的所有channel都可以收到,但是不能给group内的具体某个channel发送消息。

下面实现一个浏览器端发送消息,多个浏览器端都能接受到消息。

2.3.1官网推荐使用redis作为channel layer,所以先安装channels_redis

pip3 install channels_redis==2.4.0

2.3.2修改settings.py,添加对layer的支持

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('10.0.0.61', 6379)],
        },
    },
}
通过下面的命令检查通道是否正常工作
python3 manage.py shell
>>> import channels.layers
>>> channel_layer=channels.layers.get_channel_layer()
>>> from asgiref.sync import async_to_sync
>>> async_to_sync(channel_layer.send)('test',{'site':"www"})
>>> async_to_sync(channel_layer.receive)('test') 
{'site': 'www'}

2.3.3consumer中引入channel layer

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from channels.generic.websocket import WebsocketConsumer
import json
from asgiref.sync import async_to_sync
class ChatConsumer(WebsocketConsumer):
    def connect(self):
        # 连接时触发
        self.group_name = "chat_group"
        async_to_sync(self.channel_layer.group_add)(
            self.group_name, self.channel_name
        )
        self.accept()
    def disconnect(self, code):
        # 关闭连接时触发
        async_to_sync(self.channel_layer.group_discard)(
            self.group_name, self.channel_name
        )
        pass
    def receive(self, text_data=None, bytes_data=None):
        # 收到消息后触发
        # 真个ChatConsumer类会将所有接收到的消息加上一个"聊天"的前缀发送给客户端
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        print("wwwwwwwwwwwww", message)  # yyyyyyyyyyyyy
        async_to_sync(self.channel_layer.group_send)(
            self.group_name,
            {
                'type': 'chat_message',
                'message':message
            }
        )
    def chat_message(self,event):
        print("enventooooooooooooooooo", event)  # {'type': 'chat_message', 'message': 'yyyyyyyyyyyyy'}

        message = "聊天:" + event["message"]
        self.send(text_data=json.dumps({'message':message}))
这里我们设置了一个固定了group name,所有的消息都会发送到这个group中。
也可以通过设置参数的方式设置group name,从而创建多个group,这样可以实现仅同group内的消息互通。
注意:
    我在设置的时候,曾把[email protected]设置为组名,经debug调试发现报错,报错信息没有保留,就不展示了。
    在设置组名的时候,@符号是不可以的了。

当我们启用了channel layer之后,所有与consumer之间的通信都会变成异步的,所以必须使用async_to_sync

一个连接(channel)创建时,通过group_add将channel添加到Group中。
连接关闭时,通过group_discard将channel从Group中删除。
收到消息时,调用group_send发送消息到Group中,group中的所有channel都可以收到。

group_send中的type指定了消息处理的函数,这里会把消息交给chat_message函数处理。

经过场面的修改,我们可以看到,一个浏览器发送的数据,多个浏览器端都能收到信息。

2.4修改为异步

前面的consumer是同步的,为了更好的性能,官方支持异步的写法,只需要修改consumer.py即可

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from channels.generic.websocket import AsyncWebsocketConsumer,WebsocketConsumer
import json
from asgiref.sync import async_to_sync
from testchannel.tasks import *

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        # 连接时触发
        self.group_name = "chat_group"
        await  self.channel_layer.group_add(
            self.group_name, self.channel_name
        )
        await self.accept()
    async def disconnect(self, code):
        # 关闭连接时触发
        await self.channel_layer.group_discard(
            self.group_name, self.channel_name
        )
        pass
    async def receive(self, text_data=None, bytes_data=None):
        # 收到消息后触发
        # 真个ChatConsumer类会将所有接收到的消息加上一个"聊天"的前缀发送给客户端
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        print("wwwwwwwwwwwww", message)# yyyyyyyyyyyyy
        await self.channel_layer.group_send(
            self.group_name,
            {
                'type': 'chat_message',
                'message':message
            }
        )
    async def chat_message(self,event):
        print("enventooooooooooooooooo",event) # {'type': 'chat_message', 'message': 'yyyyyyyyyyyyy'}
        message = "聊天:" + event["message"]
        await self.send(text_data=json.dumps({'message':message}))

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)
        # self.result = add.delay(1,8)
        self.accept()
    def disconnect(self, code):
        self.result.revoke(terminate=True)
    def send_message(self,event):
        self.send(text_data=json.dumps({"message":event["message"]}))
异步的代码跟之前的差别不大,只有几个小区别:
ChatConsumer由WebsocketConsumer修改为了AsyncWebsocketConsumer
所有的方法都修改为了异步defasync def
用await来实现异步I/O的调用
channel layer也不再需要使用async_to_sync了

2.5tail查看linux服务端日志

期望的效果是前端的Log中的内容,能动态的刷新

2.5.1定义log的文件路径

这里只是例子,就写在settings.py中了,在实际应用中,是动态获取的
LOG_PATH = {
    1: '/var/log/messages',
    2: '/var/log/secure',
}

2.5.2url.py

from django.contrib import admin
from django.urls import path
from testchannel.views import *
urlpatterns = [
    path('admin/', admin.site.urls),
    path('chat/', chat, name="chat-url"),
    path('login/', login, name="login"),
    path('tail/', tail, name="tail"),
]
由于channel也是支持Django的auth认证模块,只有登录成功的用户才能操作,所以需要写个Login模块。

2.5.3views.py

from django.shortcuts import render,redirect,reverse
from django.contrib import auth
from django.contrib.auth.models import User
from django.contrib.auth.decorators import login_required
# Create your views here.
from untitled import settings
def chat(request):
    return render(request, 'index.html')
def login(request):
    # User.objects.create_user(username="vita", password="123")
    if request.method=="POST":
        user = request.POST.get("username")
        password = request.POST.get("password")
        user = auth.authenticate(username=user, password=password)
        print("mmmmmmmmmm",user)
        if user:
            auth.login(request, user)
        return redirect(reverse('tail'))
    return render(request,'login.html')

@login_required(login_url='/login/')
# 登录后才能操作
def tail(request):
    log_paths = settings.LOG_PATH
    return render(request, "tail.html", locals())

2.5.4login.html




    
    Title


{% csrf_token %}

2.5.5tail.html




    
    Title
    
    




{# 加载bootstrap的js#}

2.5.6routing.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from channels.routing import ProtocolTypeRouter,URLRouter
from channels.auth import AuthMiddlewareStack
from django.urls import path,re_path
from untitled.consumers import *

#换种写法

application = ProtocolTypeRouter({
    'websocket':AuthMiddlewareStack(
       URLRouter([
            path('ws/chat/', ChatConsumer),
            re_path(r'^ws/tailf/(?P\d+)/$', TailfConsumer),
        ])
    )
})

2.5.7consumer.py

#!/usr/bin/env python
# -*- coding:utf-8 -*-
# Author: vita
from channels.generic.websocket import AsyncWebsocketConsumer,WebsocketConsumer
import json
from asgiref.sync import async_to_sync
from testchannel.tasks import *

class TailfConsumer(WebsocketConsumer):
    def connect(self):
        self.file_id = self.scope["url_route"]["kwargs"]["id"]
                # 开始celery的异步任务
        self.result = tailf.delay(self.file_id,self.channel_name)
        # self.result = add.delay(1,8)
        self.accept()
    def disconnect(self, code):
        # 终止celery的异步任务的执行
        self.result.revoke(terminate=True)
    def send_message(self,event):
        self.send(text_data=json.dumps({"message":event["message"]}))
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。
canshu 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发送消息,官方也比较推荐这种方式

2.5.8使用celery异步循环读取日志

https://blog.51cto.com/10983441/2421459
这里有celery的说明

settings.py

INSTALLED_APPS = [

    'django_celery_beat',
    'django_celery_results',

]
CELERY_RESULT_BACKEND = 'django-db'
CELERY_BROKER_URL = 'redis://10.0.0.61:6379/1'
CELERY_TIMEZONE = 'UTC'
CELERY_ENABLE_UTC = True
CELERY_BEAT_SCHEDULER = 'django_celery_beat.schedulers:DatabaseScheduler'

# 这是使用了django-celery默认的数据库调度模型,任务执行周期都被存在你指定的orm数据库中
CELERYBEAT_SCHEDULER = 'djcelery.schedulers.DatabaseScheduler'

untitled/init.py


from __future__ import absolute_import, unicode_literals
from untitled.celery import app as celery_app

__all__ = ['celery_app']
import pymysql
pymysql.install_as_MySQLdb()

celery.py

from __future__ import absolute_import, unicode_literals
import os
from celery import Celery,platforms

# set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'untitled.settings')

app = Celery('untitled')

# Using a string here means the worker doesn't have to serialize
# the configuration object to child processes.
# - namespace='CELERY' means all celery-related configuration keys
#   should have a `CELERY_` prefix.
app.config_from_object('django.conf:settings', namespace='CELERY')

# Load task_manage modules from all registered Django testchannel configs.
app.autodiscover_tasks("")

# platforms.C_FORCE_ROOT = True

tasks.py

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
from celery import shared_task

@shared_task(name="tailf")
def tailf(id, channel_name):
    channel_layer = get_channel_layer()
    filename = settings.LOG_PATH[int(id)]
    print("llllllllllllllllllll")
    try:
        with open(filename) as f:
            # f.seek(0, 2)
            # 一开始任务总是不执行,才print调试
            # print("kkkkkkkkkk")
            while True:
                # print("dddd")
                line = f.readline()
                # print("ssssssssssss",line)
                if line:
                    print(".............",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)

@shared_task(name="add")
def add(x,y):
    print("add")
    return x+y
这里是从channels的外部发送消息给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': 'messagehahahahah'
    }
)
只需要将发送单channel的send改为group_send,channel_name改为group_name即可

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

开启worker
celery -A untitled worker --loglevel=debug

你可能感兴趣的:(django中channel模块之websocket)