프로젝트에 채팅,알람 기능을 만들기 위해 프론트와 백엔드에 websocket를 적용했다.
1. 세팅
https://channels.readthedocs.io/en/latest/ 문서 참고
2. middleware overriding
middleware.py
from channels.db import database_sync_to_async
from django.conf import settings
from django.contrib.auth.models import AnonymousUser
from channels.db import database_sync_to_async
from users.models import User
@database_sync_to_async
def get_user(user_id):
try:
return User.objects.get(id=user_id)
except User.DoesNotExist:
return AnonymousUser()
class QueryAuthMiddleware:
def __init__(self, app):
# Store the ASGI application we were passed
self.app = app
async def __call__(self, scope, receive, send):
scope["user"] = await get_user(
int(scope["query_string"].decode().split("=")[-1])
)
return await self.app(scope, receive, send)
연결 시도시 QueryAuthMiddleware가 실행되는데(세팅은 문서참고) 프론트에서 유저 아이디를 쿼리 파라미터로 받으면 해당 유저를 반환한다.
3. api작성
class MessageViewSet(viewsets.ViewSet):
def list(self, request):
try:
message = Message.objects.all()
serializer = MessageSerializer(message, many=True)
return Response(serializer.data)
except Exception as e:
print(e)
return Response([])
class NotificationViewSet(viewsets.ViewSet):
def list(self, request):
try:
notification = Notification.objects.filter(reciever=request.user).order_by(
"-created_at"
)
serializer = NotificationSerializer(notification, many=True)
return Response(serializer.data)
except Exception as e:
print(e)
return Response([])
class WebChatConsumer(websocket.JsonWebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = None
def connect(self):
self.user = self.scope["user"]
self.accept()
if not self.user.is_authenticated:
self.close(code=4001)
self.user = User.objects.get(id=self.user.id)
async_to_sync(self.channel_layer.group_add)("all_users", self.channel_name)
def receive_json(self, content):
sender = self.user
message = content["message"]
new_message = Message.objects.create(sender=sender, content=message)
async_to_sync(self.channel_layer.group_send)(
"all_users",
{
"type": "chat.message",
"new_message": {
"id": new_message.id,
"sender": new_message.sender.username,
"content": new_message.content,
"timestamp": new_message.timestamp.isoformat(),
"sender_id": new_message.sender.id,
"sender_image": new_message.sender.image,
},
},
)
def chat_message(self, event):
self.send_json(event)
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)("all_users", self.channel_name)
super().disconnect(close_code)
처음 연결될때 connect함수 실행, 인증되지 않은 사용자면 4001코드 반환하면서 연결 종료. 그렇지 않으면 async_to_sync 사용해서 그룹에 유저 추가.
프론트에서 유저가 메세지를 보내면 receive_json 함수 실행. 메세지를 Message모델에 추가하고 프론트 화면을 유저가 보기 좋게 꾸미기 위한 정보를 넣어서 chat_message함수에 event로 전달.("type": "chat.message"로 보내면 chat_message함수가 실행된다)
chat_message함수에서 받은 메세지를 프론트로 보냄. 연결이 종료될 경우 disconnect함수가 실행되어 연결이 종료된다.
@receiver(post_save, sender=Notification)
def send_update(sender, instance, created, **kwargs):
serializer = NotificationSerializer(instance)
if created:
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
str(instance.reciever.id),
{"type": "notify", "data": serializer.data},
)
class NotificationConsumer(websocket.JsonWebsocketConsumer):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = None
def connect(self):
self.user = self.scope["user"]
self.accept()
if not self.user.is_authenticated:
self.close(code=4001)
self.user = User.objects.get(id=self.user.id)
async_to_sync(self.channel_layer.group_add)(
str(self.user.id), self.channel_name
)
def receive_json(self, content):
pass
def notify(self, event):
self.send_json(event)
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
str(self.user.id), self.channel_name
)
super().disconnect(close_code)
알림 기능은 Notification모델의 정보가 db에 저장될때 실행되도록 했다. reciever 데코레이션을 통해 Notification이 생성될때 send_update 함수가 실행된다. 이 함수에서는 async_to_sync를 사용하여 알림을 보낸다.
알림을 받으면 notify 함수가 실행된다.
다음에는 프론트에서 어떻게 websocket을 연결하는지 작성할 예정.