"장고 실시간 채팅(Channels와 redis server 이용)"의 두 판 사이의 차이

Pywiki
둘러보기로 가기 검색하러 가기
225번째 줄: 225번째 줄:
 
|/chat/routing.py 작성
 
|/chat/routing.py 작성
 
|<syntaxhighlight lang="python">
 
|<syntaxhighlight lang="python">
from django.conf.urls import url
+
from django.urls import re_path
 +
 
 
from . import consumers
 
from . import consumers
  
 
websocket_urlpatterns = [
 
websocket_urlpatterns = [
     url(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer),
+
     re_path(r"ws/chat/(?P<room_name>\w+)/$", consumers.ChatConsumer.as_asgi()),
 
]
 
]
 
</syntaxhighlight>
 
</syntaxhighlight>
 
|-
 
|-
 
|라우팅 등록
 
|라우팅 등록
|/routing.py 수정
+
|channels를 설치하며 작성한 agsi.py 안에 내용을 채워준다.
 
chat 안의 라우팅을 등록해준다.
 
chat 안의 라우팅을 등록해준다.
  
그리고 임포트하는 모듈을 수정해준다.
+
그리고 이를 사용하기 위한 라이브러리를 불러온다.
 
|<syntaxhighlight lang="python">
 
|<syntaxhighlight lang="python">
 +
......
 +
 +
from channels.routing import URLRouter
 +
from channels.security.websocket import AllowedHostsOriginValidator
 
from channels.auth import AuthMiddlewareStack
 
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
 
 
import chat.routing
 
import chat.routing
  
application = ProtocolTypeRouter({
+
application = ProtocolTypeRouter(
    # (http->django views is added by default)
+
    {
    'websocket': AuthMiddlewareStack(
+
        "http": django_asgi_app,
        URLRouter(
+
        "websocket": AllowedHostsOriginValidator(
            chat.routing.websocket_urlpatterns
+
            AuthMiddlewareStack(URLRouter(chat.routing.websocket_urlpatterns))
         )
+
         ),
     ),
+
     }
})
+
)
 
</syntaxhighlight>
 
</syntaxhighlight>
|}여기까지 하고 채팅을 쳐 보면... 채팅이 나와야 정상.
+
|}여기까지 하고 점검해보자. /chat/lobby/ 에 접속한 후 채팅을 치면 떠야 한다.
 +
 
 +
같은 브라우저를 띄워두고 한쪽에서 채팅을 치면 다른 쪽에서 안뜨는데, 다른 브라우저에선 안뜬다. 접속한 모두에게 반영될 수 있도록 채널레이어를 구현한다.
  
  

2022년 10월 17일 (월) 15:09 판

1 개요

장고 채널 기능의 활용을 위해 간단한 채팅 앱 만들어보기.

1.1 사전 준비

과정 설명 방법
장고 장고가 설치되어 있다고 가정한다. 1. 장고 개요
채널 설치 채널이 설치되어 있다고 가정한다. 장고 channels

2 기초준비

2.1 채팅어플 만들기

패키지설치부터 진행하기엔 중간점검이 어려워 틀 만들기를 먼저 수행한다.

과정 설명 방법
어플리케이션생성 채팅을 위한 앱을 생성한다. django-admin startapp chat
앱 등록 settings.py에 추가.
INSTALLED_APPS = [
    ...
    'chat',
폴더 정리 - 앱을 생성하고 생성된 __init__.py와 views.py를 제외한 모든 것들을 지운다.

(채팅기능만을 위해선 나머지는 필요 없어, 지워도 된다. 근데 그냥 두자.)

- 탬플릿을 담기 위해 templates/chat 디렉토리를 생성해준다.

urls.py 생성 및 매핑 앱 내에서 사용되는 url을 다루기 위해 urls.py를 생성한다. chat/urls.py의 내용.
from django.urls import path
from . import views
app_name = 'chat'  # 보통 앱이름을 써서 url을 구분하지만, 

urlpatterns = [
    path('', views.index, name='index'),
]
기초 urls.py 수정
from django.contrib import admin
from django.urls import include, path

urlpatterns = [
    path('chat/', include('chat.urls')),
    path('admin/', admin.site.urls),
]

여기까지 하고 runserver 후 제대로 작동하는지 확인하자.

2.2 채팅룸 구현

2.2.1 채팅 인덱스 구현

들어갈 채팅룸을 입력하는 공간.

과정 설명 방법
채팅 인덱스 탬플릿 작성 채팅룸을 입력하기 위한 인덱스 탬플릿.

templates/chat/index.html로 작성하자.

<!DOCTYPE html>
<html>
<head>
    <meta charset="utf-8"/>
    <title>Chat Rooms</title>
</head>
<body>
    What chat room would you like to enter?<br>
    <input id="room-name-input" type="text" size="100"><br>
    <input id="room-name-submit" type="button" value="Enter">

    <script>
        document.querySelector('#room-name-input').focus();
        document.querySelector('#room-name-input').onkeyup = function(e) {
            if (e.keyCode === 13) {  // enter, return
                document.querySelector('#room-name-submit').click();
            }
        };

        document.querySelector('#room-name-submit').onclick = function(e) {
            var roomName = document.querySelector('#room-name-input').value;
            window.location.pathname = '/chat/' + roomName + '/';
        };
    </script>
</body>
</html>
뷰 작성
from django.shortcuts import render

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

2.2.2 채팅룸 구현

과정 설명 방법
탬플릿 작성 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>
뷰 작성 뷰에 다음 함수를 추가한다.
def room(request, room_name):
    return render(request, 'chat/room.html', {
        'room_name': room_name
    })
URL 다음의 내용을 urls.py에 추가한다.
path('<str:room_name>/', views.room, name='room'),

2.2.3 참가자 구현

과정 설명 방법
참가자 파일 작성 /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
        }))
라우팅 작성 /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.as_asgi()),
]
라우팅 등록 channels를 설치하며 작성한 agsi.py 안에 내용을 채워준다.

chat 안의 라우팅을 등록해준다.

그리고 이를 사용하기 위한 라이브러리를 불러온다.

......

from channels.routing import URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from channels.auth import AuthMiddlewareStack
import chat.routing

application = ProtocolTypeRouter(
    {
        "http": django_asgi_app,
        "websocket": AllowedHostsOriginValidator(
            AuthMiddlewareStack(URLRouter(chat.routing.websocket_urlpatterns))
        ),
    }
)

여기까지 하고 점검해보자. /chat/lobby/ 에 접속한 후 채팅을 치면 떠야 한다.

같은 브라우저를 띄워두고 한쪽에서 채팅을 치면 다른 쪽에서 안뜨는데, 다른 브라우저에선 안뜬다. 접속한 모두에게 반영될 수 있도록 채널레이어를 구현한다.


2.2.4 체널레이어 구현

과정 설명 방법
패키지 설치 Channels가 Redis인터페이스를 인식하도록. pip install channels_redis
settings.py 설정 ASGI 아래 추가하자.
ASGI_APPLICATION = 'routing.application'  # routing.py 파일의 application을 불러온다.
CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}


2.3 개요

레디스 서버를 이용한 실시간 채팅 구현

2.3.1 사전설정

과정 설명 방법
서버 설치 apt-get install redis-server
서버 테스트 서버가 잘 설치되었는지 테스트.

서버 구동에 대한 메시지가 뜬다.

redis-server
채널 라이브러리 설치 장고에서 사용할 채널 라이브러리를 설치한다. pip install -U channels
연동 라이브러리 설치 서버와 채널을 연동할 라이브러리를 설치한다. pip install channels_redis
앱을 만든다. django-admin startapp chat
앱 등록 settings.py에 추가. INSTALLED_APPS 가장 위에 'channels', 'chat'을 맨 위에 추가해준다.

다른 서드파티 앱과 충돌할 수 있어 가장 처음에 둔다.

세팅 settings.py 안에 추가.
ASGI_APPLICATION = 'config.routhing.application'

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

os.environ["DJANGO_ALLOW_ASYNC_UNDAFE"] = "true"
을 추가해준다.(대강 스테틱 설정 아래면 적당)

2.4 모델작성

chat 앱의 models.py 작성

from django.db import models
from django.conf import settings

class Room(models.Model):
    room_name = models.CharField(max_length=100, blank=True)
    users = models.ManyToManyfield(
        settings.AUTH_USER_MODEL,  # 유저모델과 연결한다.
        blank=True,
        related_name = 'rooms')  # 룸이라는 인덱스 지정.
    
    def __str__(self):
        return self.room_name
        
class Message(models.Model):
    user = models.ForeignKey(settings.AUTH_USER_MODEL, on_delete=models.CASCADE, default=1)
    room = models.ForeignKey(Room, related_name='messages', default=1, on_delete=models.CASCADE)
    content = models.TextField(blank=True)
    created_at = models.DateTimeField(auto_now_add=True)
    
    def __str__(self):
        return self.content

chat앱의 admin.py 작성

from django.contrib import admin
from .models import Room, Message

@admin.register(Room)
class RoomAdmin(admin.ModelAdmin):
    list_display = ['id', 'room_name']
    list_display_links = ['room_name']
    
@admin.register(Room)
class MessageAdmin(admin.ModelAdmin):
    list_display = ['user', 'room', 'content', 'created_at']
    list_display_links = ['user', 'room', 'content', 'created_at']

대화모델 작성(보통 친구모델 등에 작성된다.)(근데 그냥 chat 앱 안에 작성하면 안되나...?)

...
from .chat.models import Room, Message

class chat_connection(models.Model):
    target = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, on_delete=models.CASCADE)  # 상대방
    room = models.ForeignKey(Room, blank=True, on_delete=models.SET_NULL, null=True)  # null옵션이 있어야 하나..? 없어도 될듯, 강의에선 친구모델 위에 더하느라 붙인듯.
    user = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, on_delete=models.CASCADE)  # 나
    created_at = models.DateField(auto_now_add=True)
    
    def __str__(self):
        return self.user.username + "채팅"  # 사용자명을 반환한다.

2.5 뷰 작성

from django.shortcuts import render, get_object_or_404
forom django.contrib.auth import get_user_model
from chat.models import *
import logging

def start_chat(request):
    from_user =   # 본인의 유저모델 가져오기
    to_user = 유저모델.objects.get(pk=target_request_id)  # 상대의 유저모델 가져오기(이름이나 pk로 불러오게 하면 될듯)
    
    room_name = "{},{}".format(from_user.username, to_user.username)
    room = Room.objects.create(room_name=room_name)  # 룸을 만든다.
    
def chat_list(request):
    user = request.user
    user_profile = user.user_profile
    friends = user.friends.all()  # 모든 친구 불러오기...(채팅자 목록이라고 보면 됨.)
    
    context = {'user_profile': user_profile,
                'friends':friends,
    }
    
    return render(request, 'chat/chat_list.html', context)
    
def room(request, room_id):
    user = request.user
    user_profile = user.profile
    friends = user.friends.all()  # 모든 친구 불러오기...(채팅자 목록이라고 보면 됨.)
    
    room = Room.objects.get(pk=room_id)  # 룸 모델에서 해당 pk에 맞는 룸 불러오기
    friends_uer = room.users.all().exclude(pk=user.id).first()
    
    context = {'current_user': user,
        'user_profile': user_profile,
        'friends':friends,
        'room':room,
        'friends_user': friends_user,
    }
    
    return render(request, 'chat/room.html', context)

2.6 url 작성

config의 url을 넘김 처리하고..

chat앱 안의 urls.py를 작성한다.

from .views import *

app_name= 'chat'

urlpatterns = [
    path('', chat_list, name='chat_list'),
    paht('<str:room_id', room, name='room'),
]

2.7 탬플릿 작성

chat 앱 안의 templates 폴더를 만든 다음 넣는다.

<!--확장코드-->

<!--들어가야 할 것들-->
<script src="{% static 'js/messenger.js' %}"></script>

<div id="user_list">
    {% for friend in friends %}
        {% if friend.room %}
         <li name="{{ friend.room.id }}"><a href="/chat/{{ friend.room.id }}/"
         <div>{{ friend.user }}</div>
</div>

<div id="time_line">
    <div id="main_section">
        <div id="feed">
            <div id="text_field">
                <input type="text" id="txt">
                <button type="submit" id="btn">전송</button>
            </div>
        </div>
    </div>
</div>