asgi - channels - websocket

HYEYOON·2021년 7월 6일
0

실시간 알림을 보내기 위해 websocket을 사용하려고 한다.
HTTP와 다르게, 웹소켓 프로토콜은 양방향 커뮤니케이션(bi-directional communication)을 허용한다. 서버와 클라이언트 사이에 소켓연결(클라이언트 — 서버연결이 계속 유지되어 있는상태)을 지원하는 것이다!!

1. channels install

python -m pip install -U channels

나는 notification이라는 앱에서 사용할것이다.

2. settings.py 가서 추가

INSTALLED_APPS = (
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.sites',
    ...
    'channels',
    'notifiaction',
)

ASGI_APPLICATION = "프로젝트이름.asgi.application"

3. asgi.py 수정

import os

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

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    

4. consumers.py

  • consumer는 웹소켓 연결을 받는다.
  • http request를 받을 때, django는 view function을 보기 위해 root URLconf를 찾고 요청을 처리하기 위해 view function을 호출한다. 비슷하게, 채널은 web socket의 연결을 받고, consumer를 보기 위해 root routing 설정을 찾는다. 그리고 연결으로부터 온 이벤트들을 핸들링하기 위해 consumer의 여러 함수를 호출한다.
import json
from channels.generic.websocket import WebsocketConsumer

class NotiConsumer(WebsocketConsumer):
    # 웹소켓 연결, 해제 
    def connect(self):
        self.accept()
    def disconnect(self, close_code):
        pass
    def receive(self, text_data):
        # json으로 채팅 메세지 받음.
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        
        #json객체를 인코딩해서 보냄.
        self.send(text_data=json.dumps({
            'message':message
            }))
        

NotiConsumer는 동기적인 웹소켓 컨슈머로, 모든 연결을 승낙하고(accept), 클라이언트로부터 메시지를 받고(receive), 동일한 클라이언트에게 이 메시지들을 다시 돌려줍니다(send). 아직은 이 컨슈머가 같은 채팅방의 다른 클라이언트에게 메시지를 전파해주지는 못한다.

5. routing.py

[notification/routing.py]
from django.urls import re_path

from . import consumers

websocket_urlpatterns = [
        re_path(r'ws/chat/<str:user_id>/', consumers.NotiConsumer.as_asgi()),
        ]

--------------------------------------------------------------------
[프로젝트/asgi.py]
import os

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

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'leanai.settings')
 #application = get_asgi_application()

application = ProtocolTypeRouter({
    "http": get_asgi_application(),
    "websocket" : AuthMiddlewareStack(
        URLRouter(
            notification.routing.websocket_urlpatterns
            )
        ),
    })
  • as_asgi()는 as_view()와 비슷한 역할을 한다.
  • ASGI 애플리케이션을 얻기 위해 클래스 메서드인 as_asgi()를 호출했는데, 이 애플리케이션이 각 유저별 연결을 처리하는 컨슈머 인스턴스를 만들어준다.

여기까지하고 python manage.py runserver를 실행시키면

채팅 로그들이 뜨긴하지만 다른창에서 실행시키면 첫번째창 로그들이 나오지않는다. 이것을 가능하게 하려면, 동일한 ChatConsumer의 여러 인스턴스들이 서로 소통할 수 있게 해야한다. channels는 컨슈머 간에 이러한 종류의 통신이 가능하도록 추상화된 '채널 레이어'를 제공한다.

6. 채널 레이어 활성화하기

채널 레이어는 커뮤니케이션 시스템의 일종이다
채탱앱에서 여러 ChatConsumer가 통신을 주고받게하려면 ChatConsumer가 자신의 채널을 그룹에 등록해한다. 그러면 같은 방 모든 ChatConsumer한테 메시지를 보낼 수 있다.

  • 채널은 메시지가 전달되는 우편함이다. 각각의 채널은 자기 이름을 갖고 있다. 채널 이름을 알고있는 누구든 해당 채널로 메시지를 보낼 수 있다.
  • 그룹은 관련있는 채널들의 모임이다. 그룹은 이름을 갖고 있다. 그룹의 이름을 아는 누구든 새 채널을 그룹에 추가하거나, 그룹에서 제거하거나, 그룹에 등록된 모든 채널에 메시지를 보낼 수 있다. 하지만 특정 그룹에 있는 채널들을 쭉 나열하는 것은 불가능하다.

channel layer 를 구현하기 위해서 백업저장소가 필요한데, 공식문서에서는 Redis를 사용하도록 한다.

$ docker run -p 6379:6379 -d redis:5
$ python3 -m pip install channels_redis

settings에 추가.

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

쉘로 들어가서 채널레이어가 redis랑 통신할 수 있는지 확인

7. consumers.py 수정

import json
from channels.generic.websocket import WebsocketConsumer

class NotiConsumer(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
                )

    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
            }))

유저가 메시지를 전송하면, 자바스크립트 함수는 이 메시지를 웹소켓을 통해 ChatConsumer로 전송한다. ChatConsumer는 메시지를 받아, 채팅방 이름에 해당하는 그룹으로 전파한다. 같은 그룹안에 있는 (즉, 같은 채팅방 안에 있는) 모든 ChatConsumer는 그룹으로부터 메시지를 전달받아, 웹소켓을 통해 자바스크립트로 이를 돌려주고, 따라서 채팅 로그에 이 메시지가 추가된다.

  • self.scope['url_route']['kwargs']['room_name']
    웹소켓 연결을 컨슈머에게 전달한 chat/routing.py의 URL route로부터room_name 인자를 얻는다.
    모든 컨슈머는 자신의 연결에 대한 정보가 담긴 scope를 갖는다. scope 안에는 모든 URL route 인자들과, 만약 존재한다면 현재 인증된 유저 정보가 들어 있다.
  • self.roomgroup_name = 'chat%s' % self.room_name
    채널즈 그룹명을 유저가 지정한 채팅방 이름에서 다른 처리 없이 직접 구성한다.
    그룹명은 알파벳, 숫자, 하이픈, 그리고 온점만으로 구성돼야 한다.
  • async_to_sync(self.channel_layer.group_add)(...)
    그룹에 들어간다.
    async_to_sync(…)는 ChatConsumer가 동기적인 WebsocketConsumer이지만 채널 레이어의 비동기적인 메서드를 호출하고 있기 때문에 필요하다.
    그룹명은 아스키 영문자, 숫자, 하이픈, 온점으로 제한된다. 유효하지 않은 문자가 포함돼있다면 에러가 발생한다.
  • self.accept()
    웹소켓 연결을 승낙한다(Accept).
    connect() 메서드 안에서 accept()를 호출하지 않는다면, 연결 요청은 거부되고 종료된다. 요청하는 유저가 해당 동작을 수행하기 위한 권한이 없는 등의 이유로 연결을 거부할 필요가 있을 수 있다.
    연결을 승낙하려는 경우 accept()를 connect()의 마지막 동작으로 호출하도록 권장한다.
  • async_to_sync(self.channel_layer.group_discard)(...)
    그룹을 떠난다.
  • async_to_sync(self.channel_layer.group_send)
    그룹에 이벤트를 전송한다.
    이벤트는 메서드명에 대응되는 특별한 type 키를 갖고 있다. 이 이벤트를 전달받는 컨슈머들은 대응되는 메서드명의 메서드를 실행하게 된다.
profile
Back-End Developer🌱

0개의 댓글