본문 바로가기

Python/Django

[Django Channels 2.4.0] Building simple Chat Server / 채팅 서버 구현 튜토리얼 part.2 Implement a Chat Server

[Python/Django] - [Django Channels] Building simple Chat Server / 채팅 서버 구현 튜토리얼 part.1

 

[Django Channels] Building simple Chat Server / 채팅 서버 구현 튜토리얼 part.1

막연히 개인 채팅 서버를 구축해보고싶다! 해서 찾아본 django channels 라이브러리 이를 통해 HTTP 외의 일을 할 수 있다. Django Channels란? https://channels.readthedocs.io/en/latest/index.html#django-cha..

jisun-rea.tistory.com

이어서...

room view 추가하기

index view에서 채팅룸을 검색해서 들어갈 수 있다면,

room view에서 특정 채팅룸에 들어가 메세지를 볼 수 있게 해보자.

 

우선, chat/templates/chat/room.html를 생성하고 다음 코드를 넣자.

<!-- chat/templates/chat/room.html -->
<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Room</title>
</head>
<body>
    <textarea id="chat-log" cols="100" rows="20"></textarea><br>
    <input id="chat-message-input" type="text" size="100"><br>
    <input id="chat-message-submit" type="button" value="Send">
    {{ room_name|json_script:"room-name" }}
    <script>
        const roomName = JSON.parse(document.getElementById('room-name').textContent);

        const chatSocket = new WebSocket(
            'ws://'
            + window.location.host
            + '/ws/chat/'
            + roomName
            + '/'
        );

        chatSocket.onmessage = function(e) {
            const data = JSON.parse(e.data);
            document.querySelector('#chat-log').value += (data.message + '\n');
        };

        chatSocket.onclose = function(e) {
            console.error('Chat socket closed unexpectedly');
        };

        document.querySelector('#chat-message-input').focus();
        document.querySelector('#chat-message-input').onkeyup = function(e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector('#chat-message-submit').click();
            }
        };

        document.querySelector('#chat-message-submit').onclick = function(e) {
            const messageInputDom = document.querySelector('#chat-message-input');
            const message = messageInputDom.value;
            chatSocket.send(JSON.stringify({
                'message': message
            }));
            messageInputDom.value = '';
        };
    </script>
</body>
</html>

그리고, chat/views.py에서 room view를 만들자.

from django.shortcuts import render

def index(request):
    return render(request, 'chat/index.html')

# add here!
def room(request, room_name):
    return render(request, 'chat/room.html', {
        'room_name': room_name
    })
    

chat/urls.py를 수정하자.

from django.urls import path

from . import views

urlpatterns = [
    path('', views.index, name='index'),
    path('<str:room_name>/', views.room, name='room'), # room_name대로
]

Channels development server을 시작해보자.

> python manage.py runserver

http://127.0.0.1:8000/chat/lobby/ 에 접속하면 다음과 같은 메세지 log가 비어있는 채팅방이 나온다.

http://127.0.0.1:8000/chat/lobby/

"Hello" 라는 메세지를 쳐보자. 아직 아무일도 일어나지 않는다.

왜냐하면 room view는 URL ws://127.0.0.1:8000/ws/chat/lobby/인 WebSocket을 열려고 하지만

아직 WebSocket connections를 accept할 consumer을 아직 만들지 않았기 때문이다.

 

개발자 도구 console에서 확인해보면 다음과 같은 에러메세지를 볼 수 있다.

서버 연결 에러

consumer 만들기

그럼, 첫번째 consumer을 만들어보자.

Django가 HTTP request를 받으면, 먼저 root URLconf를 살펴봐서 view function을 찾아낸 다음, 이를 호출한다.

비슷하게,

Channels가 WebSocket connection을 받으면, root routing configuration를 살펴봐서 consumer을 찾아낸 다음, connection에 있는 events를 처리하기 위해 consumer에게 다양한 function을 호출한다.

 

여기서 우린, /ws/chat/ROOM_NAME/ 경로의 Websocket connections를 받는 기본적인 consumer을 만들어보자.

이는 Websocket으로부터 오는 모든 메세지를 받아서 같은 Websocket에 echo하는 동작을 한다.

 

chat/comsumers.py를 만들고 다음 코드를 넣자.

# chat/consumers.py
import json
from channels.generic.websocket import WebsocketConsumer

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

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

이는 synchronous[동기적] WebSocket consumer인데,

1) accepts all connections

2) receives messages from its client

3) echo those messages back to the same client

4) does not broadcast messages to other clients in the same room

 

part.1에서는 project를 위한 routing configuration을 만들었는데,

이번에는 app을 위한 routing configuration을 만들어보자. consumer로 가는 길을 안내해주는~

 

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),
]

* URLRouter limitationsre_path()를 쓰는것에 유의하자. https://github.com/django/channels/issues/1338

 

그리고 위의 chat.routing모듈에 root routing configuration을 포인팅해보자.

mysite/routing.py의 코드를 다음과 같이 수정하자.

from channels.auth import AuthMiddlewareStack # AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter # add URLRouter
import chat.routing # chat.routing

application = ProtocolTypeRouter({
    # (http->django views is added by default)
    'websocket': AuthMiddlewareStack(
        URLRouter(
            chat.routing.websocket_urlpatterns
        )
    )
})

Channels development server에 연결되면,

1) ProtocolTypeRouter은 connection type을 검사한다.

2) 만약 WebSocket connection(ws:// or wss://)이면 AuthMiddlewareStack로 연결된다.

3) AuthMiddlewareStack은 connection's scope을 현재 authenticated user로 채울 것이다.

4) 그리고 URLRouter로 연결된다.

5) URLRouter은 HTTP path of the connection(특정 consumer로 가는 경로)를 검사한다. (url pattern에 따라)

 

그러면 consumer의 /ws/chat/ROOM_NAME/ 가 잘 동작하는지 테스트해보자.

일단, database change를 알려주기 위해 다음 명령을 실행하자(Django’s session framework은 DB가 필요)

> python manage.py migrate

Channels development server을 시작하자.

> python manage.py runserver

http://127.0.0.1:8000/chat/lobby/ 에 접속하고 "hello"를 타이핑하면 chat log에 hello가 있는것을 볼 수 있다.

하지만 두번째 웹페이지에 같은 채팅방에 들어가면 첫번째 탭에서 봤던 chat log가 사라진 것을 볼 수 있다.

이를 해결하기 위해 Channels는 channel layer abstraction을 제공한다.

 

channel layer 사용하기

Channel layer은 communication system의 일종

이는 여러 consumer 인스터스서로와, 그리고 Django의 다른 파트들과 소통할 수 있게 한다.

 

A channel layer provides the following abstractions:

  • A channel is a mailbox where messages can be sent to. Each channel has a name. Anyone who has the name of a channel can send a message to the channel.
  • A group is a group of related channels. A group has a name. Anyone who has the name of a group can add/remove a channel to the group by name and send a message to all channels in the group. It is not possible to enumerate what channels are in a particular group.

각 consumer 인스턴스는 고유의 channel name을 자동으로 갖고있고, 이를 갖고 channel layer을 통해 소통한다.

 

우리도 여러 ChatConsumer 인스턴스를 만들고 각각의 channel을 group에 추가해야한다. 여기서 group은 room name에 따라 나누도록 한다.

Channel layer은 Backing storeRedis를 사용한다.

 

Redis를 설치하자.

Windows 환경에 설치하느랴 애먹었는데, 그 방법은 여기에!

[Python/Django] - [Redis 설치] Windows에 Redis 설치하는 법 / django channels에 사용하려다 삽질한 경험

 

[Redis 설치] Windows에 Redis 설치하는 법 / django channels에 사용하려다 삽질한 경험

Django channels 구현 중 Redis를 설치해야 했는데, 삽질을 엄청 했다.. 그 기록을 남긴다. 일단 내 환경은 Windows 10 HOME python 3.8.1 python virtualenv 처음에는 그냥 pip install redis만 하면 되는 줄 알..

jisun-rea.tistory.com

그리고 다음 명령어를 실행하자

>pip install redis

그리고 channels_redis를 설치하자. 이를 통해 Channels는 Redis와 어떻게 interface할 수 있는지 알게 된다

> python -m pip install channels_redis

마지막으로 mysite/settings.py파일에 CHANNEL_LAYERS 세팅을 해보자.

# Channels
ASGI_APPLICATION = 'mysite.routing.application'

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

 

자! 그럼 Channel layer이 Redis와 통신하는지 테스트를 한번 해보자

Django shell을 열고, 다음 명령을 실행해보자.

> python manage.py shell
>>> import channels.layers
>>> channel_layer = channels.layers.get_channel_layer()
>>> from asgiref.sync import async_to_sync
>>> async_to_sync(channel_layer.send)('test_channel', {'type': 'hello'})
>>> async_to_sync(channel_layer.receive)('test_channel')
{'type': 'hello'}

이렇게 {'type': 'hello'} 가 나오면 성공한 것이다.

Ctrl+Z, Enter을 눌러 shell에서 빠져나오자.

 

이제, Channel layer을 사용하기 위한 준비는 다 마쳤다.

chat/consumers.py파일의 내용을 아래 코드로 대체하자.

import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer

class ChatConsumer(WebsocketConsumer):
    def connect(self):
        self.room_name = self.scope['url_route']['kwargs']['room_name'] # 'room_name' 파라미터 받기
        self.room_group_name = 'chat_%s' % self.room_name # channels group name 받기

        # Join room group
        # 모든 channel layer methods는 async이기에
        # sync websocket consumer을 만들기 위해 필요한 작업
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )

        self.accept() # accept websocket connection

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

user이 메세지를 보내면, JavaScript functionWebsocket을 통해서 ChatConsumer에게 메세지를 보낼 것이다.

ChatConsumer은 메세지를 받고 이를 자신이 속한 group에 보낸다.

같은 그룹의 모든 ChatConsumer은 이 메세지를 받을 것이고, 이를 다시 Websocket을 통해 JavaScript에 보내는데, 여기서 chat log에 더해 보여지는 것이다.

 

테스트해보자!

> python manage.py runserver

http://127.0.0.1:8000/chat/lobby/에 접속하여 메세지를 입력하면 chat log가 올라가고,

두번째 탭에서 메세지를 입력하고

첫번째 탭으로 다시 돌아가보면 다음과 같이 두번째 탭에서 보낸 메세지가 로그로 올라와있는 것을 확인할 수 있다.

 

이렇게 기본적이지만 완전하게 동작하는 chat server 구축 완료!

하지만 다음 part3로 이어진다~~