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

Pywiki
둘러보기로 가기 검색하러 가기
260번째 줄: 260번째 줄:
 
같은 브라우저를 띄워두고 한쪽에서 채팅을 치면 다른 쪽에서 안뜨는데, 다른 브라우저에선 안뜬다. 접속한 모두에게 반영될 수 있도록 채널레이어를 구현한다.
 
같은 브라우저를 띄워두고 한쪽에서 채팅을 치면 다른 쪽에서 안뜨는데, 다른 브라우저에선 안뜬다. 접속한 모두에게 반영될 수 있도록 채널레이어를 구현한다.
  
 +
= 채널 레이어 구현 =
  
 
=== 체널레이어 구현 ===
 
=== 체널레이어 구현 ===
 +
consumer 인스턴스는 자동으로 유일한 channel name을 구성하기 때문에 서로 소통하려면 layer가 필요하다.
 
{| class="wikitable"
 
{| class="wikitable"
 
!과정
 
!과정
288번째 줄: 290번째 줄:
  
 
== 개요 ==
 
== 개요 ==
레디스 서버를 이용한 실시간 채팅 구현
+
레디스 서버를 이용한 실시간 채팅 구현.
 +
 
 +
꽤 오래된 버전인 5를 사용하는데, 아마도 이는.. 5에서 처음으로 consumer groups이 구현된 버전인데, 이걸로 구현했기 때문이 아닐까 싶다.
 
===사전설정===
 
===사전설정===
 
{| class="wikitable"
 
{| class="wikitable"
296번째 줄: 300번째 줄:
 
|-
 
|-
 
| 서버 설치
 
| 서버 설치
|
+
|설치 후 딱히 옵션을 변경할 내용은 없다.
|apt-get install redis-server
+
 
 +
어차피 로컬에서 모든 작동이 수행되기 때문에.
 +
|[http://id8436.iptime.org:8080/mediawiki/index.php/Radis Redis] 문서 참조.
 
|-
 
|-
 
|서버 테스트
 
|서버 테스트
303번째 줄: 309번째 줄:
 
서버 구동에 대한 메시지가 뜬다.
 
서버 구동에 대한 메시지가 뜬다.
 
|redis-server
 
|redis-server
|-
 
|채널 라이브러리 설치
 
|장고에서 사용할 채널 라이브러리를 설치한다.
 
|pip install -U channels
 
 
|-
 
|-
 
|연동 라이브러리 설치
 
|연동 라이브러리 설치
 
|서버와 채널을 연동할 라이브러리를 설치한다.
 
|서버와 채널을 연동할 라이브러리를 설치한다.
 
|pip install channels_redis
 
|pip install channels_redis
|-
 
|앱을 만든다.
 
|
 
|django-admin startapp chat
 
|-
 
|앱 등록
 
|settings.py에 추가.
 
|INSTALLED_APPS 가장 위에 'channels', 'chat'을 맨 위에 추가해준다.
 
다른 서드파티 앱과 충돌할 수 있어 가장 처음에 둔다.
 
 
|-
 
|-
 
|세팅
 
|세팅
 
|settings.py 안에 추가.
 
|settings.py 안에 추가.
 
|<syntaxhighlight lang="python">
 
|<syntaxhighlight lang="python">
ASGI_APPLICATION = 'config.routhing.application'
+
ASGI_APPLICATION = "mysite.asgi.application"  # 이부분은 채널을 설치할 때 작성한 부분.
 
 
 
CHANNEL_LAYERS = {
 
CHANNEL_LAYERS = {
     'default':{
+
     "default": {
         'BACKEND': 'channels_redis.core.RedisChannelLayer',
+
         "BACKEND": "channels_redis.core.RedisChannelLayer",
         'CONFIG': {
+
         "CONFIG": {
             "hosts": [('127.0.0.1', 6379)],
+
             "hosts": [("127.0.0.1", 6379)],
         }
+
         },
     }
+
     },
 
}
 
}
 
+
</syntaxhighlight>을 추가해준다.
os.environ["DJANGO_ALLOW_ASYNC_UNDAFE"] = "true"
+
|-
</syntaxhighlight>을 추가해준다.(대강 스테틱 설정 아래면 적당)
+
|기능 확인
 +
|리눅스 기반의 OS에서 개발과 서비스를 하고 있다면 기능의 확인이 수월하겠지만... 윈도우 기반의 OS에서 개발이 진행된다면 쉽지 않다.
 +
우측과 같이 파이썬 쉘을 통해 서버의 실행을 점검해보자.
 +
|<syntaxhighlight lang="bash">
 +
python3 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'}
 +
</syntaxhighlight>
 
|}
 
|}
 
==모델작성==
 
==모델작성==

2022년 10월 17일 (월) 16:36 판

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/ 에 접속한 후 채팅을 치면 떠야 한다.

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

3 채널 레이어 구현

3.1 체널레이어 구현

consumer 인스턴스는 자동으로 유일한 channel name을 구성하기 때문에 서로 소통하려면 layer가 필요하다.

과정 설명 방법
패키지 설치 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)],
        },
    },
}


3.2 개요

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

꽤 오래된 버전인 5를 사용하는데, 아마도 이는.. 5에서 처음으로 consumer groups이 구현된 버전인데, 이걸로 구현했기 때문이 아닐까 싶다.

3.2.1 사전설정

과정 설명 방법
서버 설치 설치 후 딱히 옵션을 변경할 내용은 없다.

어차피 로컬에서 모든 작동이 수행되기 때문에.

Redis 문서 참조.
서버 테스트 서버가 잘 설치되었는지 테스트.

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

redis-server
연동 라이브러리 설치 서버와 채널을 연동할 라이브러리를 설치한다. pip install channels_redis
세팅 settings.py 안에 추가.
ASGI_APPLICATION = "mysite.asgi.application"  # 이부분은 채널을 설치할 때 작성한 부분.
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}
을 추가해준다.
기능 확인 리눅스 기반의 OS에서 개발과 서비스를 하고 있다면 기능의 확인이 수월하겠지만... 윈도우 기반의 OS에서 개발이 진행된다면 쉽지 않다.

우측과 같이 파이썬 쉘을 통해 서버의 실행을 점검해보자.

python3 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'}

3.3 모델작성

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 + "채팅"  # 사용자명을 반환한다.

3.4 뷰 작성

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)

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

3.6 탬플릿 작성

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>