수학과의 좌충우돌 프로그래밍

[Django] Channels, 비동기적 채팅 구현하기 - WebSocket (2) 본문

웹프로그래밍/Django

[Django] Channels, 비동기적 채팅 구현하기 - WebSocket (2)

ssung.k 2019. 7. 11. 14:18

저번 시간 WebSocket (1)에서는 이론적인 개념들과, 약간의 실습을 진행해보았습니다. 하지만 아직 채팅은 보내지지 않고 에러가 발생하였습니다. 지금부터 그 문제를 해결해보도록 하겠습니다.

 

  • 소비자 만들기

    django의 기본 원리를 생각해보면 HTTP 요청을 받아들이고 매핑된 URL 로 이동, 이에 따라 views 에 함수를 실행합니다. 이와 유사하게 Channels역시 WebSocket 연결을 받아들이면, root routing configuration에서 소비자를 찾은 후에, 이벤트를 처리하기 위한 함수들을 호출합니다.

    # chat/consumers.py
    
    from channels.generic.websocket import WebsocketConsumer
    import json
    
    class ChatConsumer(WebsocketConsumer):
      	# websocket 연결 시 실행
        def connect(self):
            self.accept()
    		# websocket 연결 종료 시 실행 
        def disconnect(self, close_code):
            pass
    		# 클라이언트로부터 메세지를 받을 시 실행
        def receive(self, text_data):
            text_data_json = json.loads(text_data)
            message = text_data_json['message']
    				# 클라이언트로부터 받은 메세지를 다시 클라이언트로 보내준다.
            self.send(text_data=json.dumps({
                'message': message
            }))
    
  • routing

    소비자 라우팅을 처리하기 위해서 앱 안에 routing 을 추가해줍니다. 위에서 말했듯이 django의 url 과 유사합니다.

    # chat/routing.py
    
    from django.conf.urls import url
    from . import consumers
    
    websocket_urlpatterns = [
        url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer),
    ]
    

    routing파일을 django가 인식할 수 있도록 추가해줘야합니다.

    # mysite/routing.py
    
    from channels.auth import AuthMiddlewareStack
    from channels.routing import ProtocolTypeRouter, URLRouter
    import chat.routing
    
    # 클라이언트와 Channels 개발 서버가 연결 될 때, 어느 protocol 타입의 연결인지
    application = ProtocolTypeRouter({
        # (http->django views is added by default)
      	# 만약에 websocket protocol 이라면, AuthMiddlewareStack
        'websocket': AuthMiddlewareStack(
            # URLRouter 로 연결, 소비자의 라우트 연결 HTTP path를 조사
            URLRouter(
                chat.routing.websocket_urlpatterns
            )
        ),
    })
    

     

    ERROR - server - Exception inside application: no such table: django_session

    다음과 같은 에러가 발생한다면,

    python manage.py migrate
    

    migrate 함으로서 해결 할 수 있습니다.

 

여기 까지 진행 하였다면 사용자가 작성하는 메세지, 즉 클라이언트에서 넘어온 메세지를 다시 클라이언트 쪽으로 보내서 메세지를 보내는 듯한 효과를 줍니다. 하지만 아직 같은 채팅방에 있더라도 다른 사용자는 메세지를 볼 수 없습니다.

(1) 에서 설명했던 간단히 알아보았던 Channel layer 를 사용하면 소비자들끼리 소통을 가능하게 할 수 있습니다.

 

Channel layer 란?

Channel layer는 의사소통 시스템 입니다. 즉 많은 소비자들 더 나아가서는 django의 다른 부분과 의사소통을 할 수 있게 해줍니다. Channel layer 에는 두 가지 개념이 존재합니다. channel 과 group 입니다.

  • channel

    channel 은 메세지를 보낼 수 있는 우편함이라고 생각하면 됩니다. 각 채널은 고유한 이름이 있으며 누구든지 채널에 메세지를 보낼 수 있습니다.

  • group

    group 은 채널과 관련된 그룹입니다. 그룹도 마찬가지로 이름을 가지며 그룹 이름을 가진 사용자는 누구나 그룹에 채널을 추가 / 삭제 가능하고 그룹의 모든 채널에게 메세지를 보낼 수 있습니다. 그러나 그룹에 속한 채널을 나열 할 수 없습니다.

소비자들을 채널 이름을 하나씩 가지고 있으며 Channel layer 를 통해 메세지를 주고 받을 수 있습니다.

 

  • channel layer 구현

    이어서 실습을 진행해보도록 하겠습니다. channel layer 를 구현하기 위해서 백업저장소가 필요한데, Redis를 사용하도록 하겠습니다. 이를 사용하기 위해서는 별도의 설치가 필요합니다.

     

    macOS 용 패키지 관리자 Tool 인 brew 를 이용해서 설치해주었습니다.

    $ brew install redis
    

    다음으로 redis server 를 돌려줍니다.

    $ redis-server
    

    그리고 channels 가 redis 인터페이스를 얻을 수 있도록 channels_redis 패키지를 설치합니다.

    $ pip install channels_redis
    

    다음으로는 settings 에 channel layer의 설정을 해줍니다.

    # Channels
    ASGI_APPLICATION = 'mysite.routing.application'
    CHANNEL_LAYERS = {
        'default': {
            'BACKEND': 'channels_redis.core.RedisChannelLayer',
            'CONFIG': {
                "hosts": [('127.0.0.1', 6379)],
            },
        },
    }
    

    이번엔 기존에 작성했던 consumers 를 추가해줍니다.

    # chat/consumers.py
    from asgiref.sync import async_to_sync
    from channels.generic.websocket import WebsocketConsumer
    import json
    
    class ChatConsumer(WebsocketConsumer):
        def connect(self):
          	# chat/routing.py 에 있는
            # url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer),
            # 에서 room_name 을 가져옵니다.
            self.room_name = self.scope['url_route']['kwargs']['room_name']
            self.room_group_name = 'chat_%s' % self.room_name
    
            # 그룹에 join
            # send 등 과 같은 동기적인 함수를 비동기적으로 사용하기 위해서는 async_to_sync 로 감싸줘야한다.
            async_to_sync(self.channel_layer.group_add)(
                self.room_group_name,
                self.channel_name
            )
            # WebSocket 연결
            self.accept()
    
        def disconnect(self, close_code):
            # 그룹에서 Leave
            async_to_sync(self.channel_layer.group_discard)(
                self.room_group_name,
                self.channel_name
            )
    
        # WebSocket 에게 메세지 receive
        def receive(self, text_data):
            text_data_json = json.loads(text_data)
            message = text_data_json['message']
    
            # room group 에게 메세지 send
            async_to_sync(self.channel_layer.group_send)(
                self.room_group_name,
                {
                    'type': 'chat_message',
                    'message': message
                }
            )
    
        # room group 에서 메세지 receive
        def chat_message(self, event):
            message = event['message']
    
            # WebSocket 에게 메세지 전송
            self.send(text_data=json.dumps({
                'message': message
            }))
    

     

macOS용 패키지 관리자 Tool

macOS용 패키지 관리자 Tool

이전 포스팅에서의 문제는 메세지 송 수신은 되지만 다른 사용자에게는 보이지 않는다는 점이었습니다. 이번에는 channels layer 를 사용하여 이러한 문제를 해결해보았습니다. 브라우저를 두 개 띄워놓고 같은 채팅방에 입장한 후, 메세지를 전송해보면 양 쪽에서 메세지가 뜨는 걸 확인할 수 있습니다. 제대로 채팅방이 구현된 것이죠.

Comments