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推荐使用的包
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.