django channels& websocket

Yeseul Han·2022년 8월 16일
0

🔥 요약 및 이론 🔥

요약

약 1달 전에 했던 장고 채널 웹소켓 튜토리얼을 다시해보면서 복습 및 정리를 해본다.
다큐멘테이션과 튜토리얼 영상 그리고 내 머리속을 참고했다.

channels란

http 프로토콜이 아닌 다른 프로토콜을 사용할 수 있게 해주는 장고 기능이다.
WebSockets, chat protocols, IoT protocols 등을 다룰 수 있고, ASGI 위에 빌드되는게 특징이다. (배포 단계에서는 이게 굉장히 중요해진다)

ASGI(Asynchronous Server Gateway Interface)란

ASGI는 WSGI를 계승한다. 그 말은 사실상 WSGI랑 비슷한 역할을 한다고 볼 수 있다는 거다. 다만 WSGI는 동기어플리케이션을 다룬다면 ASGI는 동기 및 비동기 어플리케이션에 해당하는 프로토콜을 제공한다.

websocket이란

🔥 GAME START 🔥

순서

  • Configure ASGI
  • Consumers
  • Routing
  • Websockets

< Configure ASGI >

0. pip install channels

pip install channels

1. setting.py installed _apps에 channels 추가

INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    "chat", 
    "channels",
]

2.프로젝트앱/asgi.py에 routing configuration 생성

  • routing configuration이란 채널 서버에서 http리퀘스트를 받았을때 어떤 코드를 실행할지 지정해주는 asgi어플리케이션이다.

# 프로젝트앱/asgi.py
import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ws.settings")

application = ProtocolTypeRouter({
    'http': get_asgi_application()
})

다시 settings.py로 가서 asgi로 연결될 어플리케이션이 이 프로젝트임을 지정해주자.
여기서 ws는 프로젝트 이름이니까 자기에게 맞게 바꾸면 된다.

# 프로젝트앱/settings.py
ASGI_APPLICATION= 'ws.asgi.application'

여기까지 한 뒤에는 마이그레이션을 하고 runserver를 해보자

python manage.py migrate
python manage.py runserver

asgi가 잘 설정되었다면 터미널창에 이렇게 볼 수 있다 :)

< javascript 소켓 커넥션 >

우리에게 익숙한 http와 다른 ws 형식의 url에 당황하게 되는데, 걱정할 것 없다. http만 ws로 바꿔준 것이지 익숙한 url형식이다. 메세지가 오면 바로 데이타를 파싱해서 콘솔로그로 찍도록 했다.

        let url = `ws://${window.location.host}/ws/socket-server/`
        const chatSocket = new WebSocket(url)
        chatSocket.onmessage = function(e){
            let data = JSON.parse(e.data)
            console.log('Data:', data)
        }

이 url을 핸들할 백엔드가 구성되지 않아 콘솔창에는 커넥션 에러가 찍힌다.

< consumers >

위에서 말하는 백엔드란 1차적으로 consumers.py다.
consumers는 channels의 views.py라고 생각하면 쉽다.
view와의 차이는 리퀘스트를 받기만 하는게 아니라 리퀘스트를 백엔드에서 프론트엔드로 주기도 한다는 것이다. 이 점이 정말 매력적이라고 생각한다.

1. consumers.py 생성

chat기능이 있는 앱에 consumers.py를 만들어주자.

2. consumers.py 작성

views를 만들듯이 클래스를 만들고 그 안에 첫번째 함수를 connect로 두었다.

websocket요청이 오면 먼저 accept함수를 사용해서 요청받은 소켓을 받는다.

그리고 send 함수를 사용해서 텍스트 데이타를 다시 보내 확인 할 수 있게 했다. json.dumps 뒤에 있는 타입이랑 메시지는 고정값이 아니니 원하는 대로 써보자.

import json
from channels.generic.websocket import WebsocketConsumer

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.accept()

        self.send(text_data=json.dumps({
            'type': 'connection_established',
            'message': 'You are now connected!'
        }))

< Routing >

1. routing.py 작성

같은 앱에 이번엔 routing.py 파일을 만들고, websocket urlpatterns를 작성해보자. 웹소켓 url패턴이 프론트엔드의 url이 맞는지 확인해보자.

참고로 여기서 사용되는 re_path는 옛날에 쓰이던 url 형식인데... 장고 2.0 이전에는 우리가 익숙한 path가 아닌 정규식을 사용한 url url패턴을 사용했다. 이후 2.0이 되면서 훨씬 간결해진 path를 쓰지만 정규식을 사용한 옛날 방식의 url패턴이 필요할땐 re_path를 사용한다.

from django.urls import re_path
from . import consumers

websocket_urlpatterns=[
    re_path(r'ws/socket-server/', consumers.ChatConsumer.as_asgi())
]

2. AuthMiddlewareStack

장고 인증과 세션과 저장될 정보를 다룬다. AuthMiddlewareSessionMiddlewareCookieMiddleware를 필요로 하는데 이 모든걸 한번에 담은게 바로 AuthMiddlewareStack!

# asgi.py
import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
import chat.routing

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ws.settings")

application = ProtocolTypeRouter({
    'http': get_asgi_application(),
    'websocket':AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    )
})

장고 세션은 데이터베이스를 필요로 하기 때문에 이 시점에서 마이그레이션을 해주자. 이후 바로 runserver를 하고 프론트 콘솔창을 가면!! 연결이 되었다

악수~~~🤝🤝🤝

🔥 Next Round 🔥

recieve

어렵지 않다. chat/consumers.py ChatConsumers 클래스에 receive함수를 추가해줬다. 받은 text_data를 풀어서 프린트를 하는 간단한 코드다.

    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        print('Message:', message)
//프론트
        let form = document.getElementById('form')
        form.addEventListener('submit', (e)=>{
            e.preventDefault()
            let message = e.target.message.value
            chatSocket.send(JSON.stringify({
                'message': message
            }))
            form.reset()
        })

실행해보면 아주 잘찍힌다.

recieve2

이젠 서버로 받은 메세지를 모든 챗 룸 유저들이 받을 수 있도록 다시 프론트로 보내보자.

    def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        print('Message:', message)

        self.send(text_data=json.dumps({
            'type':'chat',
            'message':message
        }))
//프론트
        let url = `ws://${window.location.host}/ws/socket-server/`
        const chatSocket = new WebSocket(url)
        chatSocket.onmessage = function(e){
            let data = JSON.parse(e.data)
            console.log('Data:', data)
            if (data.type ==='chat'){
                let messages = document.getElementById('message')

                messages.insertAdjacentHTML('beforeend', `<div><p>${data.message}</p></div>`)
            }
        }

Done!!

🔥 Final Round 🔥

< Channel Layers >

여러개의 채팅방이 기능하려면 channel layers를 사용해야 한다.
여기서 채널이란 유저에게 딸린 우체통이라고 생각하면 좋다. 채널들이 모여서 여러 유저가 소통하는 하나의 채팅방 그룹이 만들어지고 이게 다시 데이터베이스에 들어가 관리된다.

1. setting.py에 channel_layers 추가

여기선 inmemorychannellayer를 사용했지만 실제 환경에선 레디스..같은 db를 사용하는게 좋다고 한다. inmemorychannellayer는 디버그 모드 온리라고ㅇㅇ...레디스 연구해볼 필요가 있겠다.


CHANNEL_LAYERS = {
    'default':{
        'BACKEND':'channels.layers.InMemoryChannelLayer'
    }
}

1. consumers.py

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.room_group_name = 'test'

        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )
        self.accept()

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

        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name,
            {
                'type':'chat_message',
                'message':message
            }
        )
        
    def chat_message(self,event):
        message = event['message']

        self.send(text_data=json.dumps({
            'type':'chat',
            'message':message
        }))
profile
코딩 잘하고 싶다

0개의 댓글