博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

django channels实现websocket的一个问题

Posted on 2022-06-14 16:13  Sxcan  阅读(1945)  评论(0编辑  收藏  举报
背景

需要做一个能够实时输出运行结果的界面,美滋滋的做完后,突然发现,发起websocket请求后,在这个请求未结束之前,无法发起其他请求,不管是http还是websocket,都会阻塞。。。无语凝噎。google无果,官方文档查看也没看到啥有用的东西(也可能是我急于解决问题,没有细看),后来看了下参考的文章,发现其django、channels版本都比我的低,于是尝试降版本测试,可行了。。。我一个小小的运维,何苦遭此折磨。。。

我的版本
Django            3.2
channels          3.0.4
channels-redis    3.4.0
参考版本
Django              2.0.5
channels            2.1.1
channels-redis      2.2.1

推测是channels升级到3.0版本后有了大的改动,使用方法和之前不一样了。。肯定是往好的方向走,但是我并没有找到类似的解决案例。有遇到过知道如何解决的小伙伴,动动小手留下你的解决方案吧!

20220614更新:
好家伙,django 2.0.5有个问题,在create数据时会有如下报错,升级到2.2.28就没问题。。。为啥升到这个版本?因为机智的我先安装了channels2.1.1,这货直接把django3.2给我卸载干成了2.2.28,我一试,您猜怎么着,成了!

 File "/home/MonitorTools/python3.8.6/lib/python3.8/site-packages/django/db/backends/oracle/operations.py", line 228, in fetch_returned_insert_id
    return int(cursor._insert_id_var.getvalue())
TypeError: int() argument must be a string, a bytes-like object or a number, not 'list'

20220616更新:
本以为没有了后续,谁知问题才刚刚开始。。。降版本后又出现了这种问题,于是又是各种折腾,贴下报错

Application instance <Task pending name='Task-2' coro=<StaticFilesWrapper.__call__() running at /home/python/.pyenv/versions/testChannels/lib/python3.8/site-packages/channels/staticfiles.py:44> wait_for=<Future pending cb=【<TaskWakeupMethWrapper object at 0x7f4af0b59a30>()】>> for connection <WebSocketProtocol client=【'IP地址', 54090】 path=b'/room/123/'> took too long to shut down

是的,发现降版本还报错后我又升回了django3和channels3,经过反复测试,有那么一两会儿是正常的,后来还是阻塞,说这么多,还是贴下代码吧

import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
import datetime
import time


class ChatConsumer(WebsocketConsumer):
    def websocket_connect(self, message):
        self.accept()
        print('有人来连接了')

    def websocket_disconnect(self, message):
        print('断开连接')
    
    def websocket_receive(self, message):
        # self.cope
        msg = message['text']
        print('接收消息:' + msg)
        for i in range(30):
            self.send(msg+ str(i) +'.xasdsfsd')
            time.sleep(1)

因为之前写的代码里有些操作是耗时的,这里没有啥好办法去模拟,就直接time.sleep了,排除过程中看了channels配置,消费者写法,网上各种教程,换了百种写法都无果。后来看到了报错中packages/channels/staticfiles.py,于是点进去想看看报错took too long to shut down的逻辑,结果没找到。。。于是盲猜是不是静态文件的问题,因为在测试过程中,每次出问题貌似就是我在强制清空缓存后,于是乎在网上找了方法,专门使用daphne来监听websocket,后来测试,果然没问题了。。。你以为这就结束了?是的,测了几把有报错了。。。原汁不原味。。。这次在python3.8/site-packages/channels/routing.py上报错了。。

2022-06-16 16:48:48,593 WARNING  Application instance <Task pending name='Task-49' coro=<ProtocolTypeRouter.__call__() running at /home/user/.pyenv/versions/3.8.6/envs/project/lib/python3.8/site-packages/channels/routing.py:71> wait_for=<Future pending cb=[_chain_future.<locals>._call_check_cancel() at /home/user/.pyenv/versions/3.8.6/lib/python3.8/asyncio/futures.py:360, <TaskWakeupMethWrapper object at 0x7f9aeaa11790>()]>> for connection <WebSocketProtocol client=['IP', 61617] path=b'/ws/clean/'> took too long to shut down and was killed.

后来看到知乎上有个大佬写的一篇python asgi的文章,里面随手带过的一句话引起了我的注意不能使用time.sleep, 会阻塞整个线程,而我的测试恰恰用的都是time.sleep,于是乎接着搜索,又看到了另一篇文章
https://string.quest/read/16973226, 里面提到通过生产者-消费者模式,每个消费者依次消费队列:从队列中获取>进程>从队列中获取... 在给定项目(或“事件”)的流程阶段,您将消费者置于 sleep 状态,因此该流程在 sleep 完成之前不会完成。您不能消费下一个项目,这就是您的消费者被阻止的原因。,这么一看,好像问题就是在我用了time.sleep导致的,事实上我在测试时,如果不用time.sleep时确实不会造成阻塞,但是仍会有报错。
再后来有幸在一位大哥的博客上看到类似的经历:
https://j-sui.com/2020/05/20/django-channels-daphne-tricky-exception/
得出的结论是daphne在报错,去看了源码,是因为关闭websocket时超时导致的错误提示。。先写到这里,后面接着测试。。。

20220617更新:
时至今日,终于搞定了。思路参考https://blog.ops-coffee.cn/s/r5spytjrl0jjeauye4q_-q ,大概就是celery执行任务,通过channels_layers实时发送消息至WEB。因为任务执行都放在了celery异步执行,所以对于websocketconsumer来说并没有任何需同步等待的操作,就不会造成阻塞。
阻塞的操作不止是time.sleep,数据库读写、os操作等等可能会耗时的都可能会造成阻塞。官方有相关的说明,基础功底不够扎实,又心浮气躁,没有去细细查阅。感兴趣的可以通读研究一下, https://channels-readthedocs-io.translate.goog/en/stable/topics/databases.html?_x_tr_sl=auto&_x_tr_tl=zh-CN&_x_tr_hl=zh-CN&_x_tr_pto=wapp
贴下我的代码:
以下代码基于环境:

python   3.8.6
django   4.0.5
channels 3.0.4

首先是channels的配置

# settings.py
INSTALLED_APPS = [
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    'channels',
]

ASGI_APPLICATION = "project.asgi.application"

# channel配置
CHANNEL_LAYERS = {
    "default": {
        # This example app uses the Redis channel layer implementation channels_redis
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            # "hosts": [('127.0.0.1', 7779)],
            "hosts": [("redis://xxx.xx.xx.xx:8000/2")],
        },
    },
}

asgi路由

# project/asgi.py settings.py同级目录下
import os
import django
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
django.setup()
from django.core.asgi import get_asgi_application
from django.urls import path
from channels.routing import ProtocolTypeRouter, URLRouter
from clean.consumers import CleanConsumer



application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    # Just HTTP for now. (We can add other protocols later.)
    "websocket":
        URLRouter([
            path(r'ws/clean/', CleanConsumer.as_asgi()),
        ]),
})

consumer

# myapp/consumers.py
from channels.generic.websocket import WebsocketConsumer
from myapp.tasks import mytask
class MyConsumer(WebsocketConsumer):

    def __init__(self, *args, **kwargs):
        super(MyConsumer, self).__init__(*args, **kwargs)
        self.redis = get_redis_connection()

    def connect(self):
        self.accept()

    def receive(self, text_data=None, bytes_data=None):
        info = json.loads(text_data)
	# 调用celery任务
        self.result = mytask.delay(info, self.channel_name) # self.channel_name是websocketconsumer本身具有的一个属性

    def disconnect(self, close_code):
        # self.result.revoke(terminate=True)

    def send_message(self, event):
	#自定义函数,在celery任务中使用channels_layers会用到
        self.send(text_data=json.dumps(event))

celery task

# myapp/tasks.py
from __future__ import absolute_import, unicode_literals
from asgiref.sync import async_to_sync
from channels.layers import get_channel_layer
from project.celery import app

@app.task
def auto_expdp(info, channel_name):
    # 通过channels_layers从websocketconsumer函数外部向channels发送消息,
    channel_layer = get_channel_layer()
    # 异步转同步使用async_to_sync方法,channel_name为频道名,这里直接用的websocketconsumer自带的channel_name属性,type发送的方法,send.message对应前面MyConsumer里的send_message,这里channels是对.进行了内部转化为_,没啥特殊理解的,message就是你要发送的消息
    async_to_sync(channel_layer.send)(
                        channel_name,
                        {"type": "send.message","message": ''}
                    )

以上基本上就成型了,可以异步执行任务,然后页面实时输出每个步骤的执行结果。官方给出的其他consumer如SyncConsumer,还未深入研究,待有时间研究后再来更新。

20220725更新:
记录一个channels配置报错的问题,配置好channels后启动django就会报错如下
django.core.exceptions.ImproperlyConfigured: Cannot import ASGI_APPLICATION module 'dbaOps.asgi'
解决方法:

# 进入调试
python manage.py shell
# 尝试导入,根据报错进行处理,这里是某个文件导入某个模块有问题
from dbaOps.asgi import application