Django Websocket实现日志展示

514 阅读2分钟

Websocket实现日志展示

技术选型:

需求

前端需要展示日志文件, 传统的轮训方式效果不好, 并且造成资源浪费

技术选型

  • Django==4.1
  • celery==5.2.3
  • channels==3.0.5
  • channels-redis==3.4.1
  • gunicorn==20.1.0
  • uvicorn[standard]==0.18.2

django websocket有很多支持的第三方包, 如下所示

djangopackages.org/grids/g/web…

awssome推荐使用的包

github.com/wsvincent/a…

channels官方文档

channels.readthedocs.io/en/stable/

由于channels它建立在名为ASGI的 Python 规范之上。因此选用uvicorn[standard], 注意并非uvicorn

生产环境下uvicorn 采用gunicorn进行部署 www.uvicorn.org/deployment/

入门

实现简易版聊天室 juejin.cn/post/713273…

实现日志展示

基于简易聊天室的代码chat应用

安装并配置celery

pip install celery==5.2.3

新建mysite/celery_app.py

#mysite/celery_app.py
import os

from celery import Celery

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

app = Celery("Django")

# 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 modules from all registered Django app configs.
app.autodiscover_tasks()
# mysite/settings.py
CELERY_BROKER_URL = "redis://:password@0.0.0.0:6379/0"
CELERY_RESULT_BACKEND = CELERY_BROKER_URL
#mysite/__init__.py
from .celery_app import app as celery_app

__all__ = ("celery_app",)

创建task任务

from celery import shared_task
from channels.layers import get_channel_layer
from asgiref.sync import async_to_sync
import time


@shared_task
def tailf(filename, channel_name):
    channel_layer = get_channel_layer()
    try:
        with open(filename) as f:
            # f.seek(0, 2)
            while True:
                line = f.readline()
                if 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)

创建路由

# chat/routing.py
from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
    re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()),
    re_path(r'ws/tailf/(?P<filename>\w+)/$', consumers.TailFConsumer.as_asgi()),
]
# chat/consumers.py
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
from .tasks import tailf


class TailFConsumer(WebsocketConsumer):
    def connect(self):
        self.filename = self.scope['url_route']['kwargs']['filename']
        print(self.filename)
        self.result = tailf.delay(f"/tmp/{self.filename}", self.channel_name)
        print(self.result)
        self.accept()

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

    def send_message(self, event):
        self.send(text_data=json.dumps({"message": event["message"]}))


class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name']
        self.room_group_name = 'chat_%s' % self.room_name

        # Join room group
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )

        self.accept()

    def disconnect(self, close_code):
        # Leave room group
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name
        )

    # Receive message from WebSocket
    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']

        # Send message to room group
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                'type': 'chat_message',
                'message': message
            }
        )

    # Receive message from room group
    def chat_message(self, event):
        message = event['message']

        # Send message to WebSocket
        self.send(text_data=json.dumps({
            'message': message
        }))

启动主进程

python3 manage.py runserver

启动celery

celery -A mysite.celery_app worker -l INFO

自定义认证

https://stackoverflow.com/questions/65297148/django-channels-jwt-authentication

创建channelsmiddleware.py

"""General web socket middlewares
https://stackoverflow.com/questions/65297148/django-channels-jwt-authentication
"""

from channels.db import database_sync_to_async
from django.contrib.auth.models import AnonymousUser
from rest_framework_simplejwt.exceptions import InvalidToken
from channels.middleware import BaseMiddleware
from channels.auth import AuthMiddlewareStack
from django.db import close_old_connections
from urllib.parse import parse_qs

from app.utils.request.cmdb import CmdbApi
from app.utils.models import DxyTokenUser


@database_sync_to_async
def get_user(raw_token):
    try:
        pass
    except InvalidToken:
        return AnonymousUser()


class JwtAuthMiddleware(BaseMiddleware):
    def __init__(self, inner):
        self.inner = inner

    async def __call__(self, scope, receive, send):
        # Close old database connections to prevent usage of timed out connections
        close_old_connections()

        # Get the token
        print(scope)
        print(scope["query_string"])
        token = parse_qs(scope["query_string"].decode("utf8"))["token"][0]
        print("token:---{}".format(token))
        # Try to authenticate the user
        scope["user"] = await get_user(raw_token=token)
        return await super().__call__(scope, receive, send)


def JwtAuthMiddlewareStack(inner):
    return JwtAuthMiddleware(AuthMiddlewareStack(inner))

consumer.py

def connect(self):
    self.user = self.scope['user']
    self.result = None
    if not self.user.is_authenticated:
        self.close(4001)

gunicorn 部署

gunicorn + uvicorn 部署下载的是

  • gunicorn==20.1.0
  • uvicorn[standard]==0.18.2
gunicorn -k uvicorn.workers.UvicornWorker mysite.asgi

注意: asgi.py文件中如果引用了路由

django_asgi_app = get_asgi_application()
from chat.routing import websocket_urlpatterns  # noqa

需在django_asgi_app 下方引入, 否则可能报错:

django.core.exceptions.AppRegistryNotReady: Apps aren't loaded yet.

stackoverflow.com/questions/5…