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

Pywiki
둘러보기로 가기 검색하러 가기
 
(같은 사용자의 중간 판 16개는 보이지 않습니다)
46번째 줄: 46번째 줄:
 
|urls.py 생성 및 매핑
 
|urls.py 생성 및 매핑
 
|앱 내에서 사용되는 url을 다루기 위해 urls.py를 생성한다.
 
|앱 내에서 사용되는 url을 다루기 위해 urls.py를 생성한다.
|urls.py의 내용.<syntaxhighlight lang="python">
+
|chat/urls.py의 내용.<syntaxhighlight lang="python">
 
from django.urls import path
 
from django.urls import path
 
from . import views
 
from . import views
191번째 줄: 191번째 줄:
 
</syntaxhighlight>
 
</syntaxhighlight>
 
|}
 
|}
=== 참가자 구현 ===
+
=== 참가자(컨슈머) 구현 ===
 
{| class="wikitable"
 
{| class="wikitable"
 
!과정
 
!과정
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/ 에 접속한 후 채팅을 치면 떠야 한다.
  
 +
같은 브라우저를 띄워두고 한쪽에서 채팅을 치면 다른 쪽에서 안뜨는데, 다른 브라우저에선 안뜬다. 접속한 모두에게 반영될 수 있도록 채널레이어를 구현한다.
  
=== 체널레이어 구현 ===
+
= 채널 레이어 구현 =
 +
consumer 인스턴스는 자동으로 유일한 channel name을 구성하기 때문에 서로 소통하려면 layer가 필요하다.
 +
 
 +
== 레디스 ==
 +
레디스 서버를 이용한 실시간 채팅 구현.
 +
 
 +
꽤 오래된 버전인 5를 사용하는데, 아마도 이는.. 5에서 처음으로 consumer groups이 구현된 버전인데, 이걸로 구현했기 때문이 아닐까 싶다.(최신버전에서도 될진.. 검증이 필요하다.)
 +
===사전설정===
 
{| class="wikitable"
 
{| class="wikitable"
 
!과정
 
!과정
261번째 줄: 273번째 줄:
 
!방법
 
!방법
 
|-
 
|-
|패키지 설치
+
| 서버 설치
|Channels가 Redis인터페이스를 인식하도록.
+
|설치 후 딱히 옵션을 변경할 내용은 없다.
 +
어차피 로컬에서 모든 작동이 수행되기 때문에.
 +
 
 +
네이티브로 설치하는 게 아니라 도커를 사용할 거라면 아래로 넘어가자.
 +
 
 +
단순 apt-get install redis로 설치해 5 이상의 버전이 되어도 작동한다.
 +
|[http://id8436.iptime.org:8080/mediawiki/index.php/Radis Redis] 문서 참조.
 +
|-
 +
|도커
 +
|도커가 설치되어 있다면 아래 명령으로 바로 설치, 서비스가 시작된다.
 +
 
 +
docker run -p 6379:6379 -d redis:5
 +
|아래 문서를 통해 도커 설치를 참고하자.
 +
http://id8436.iptime.org:8080/mediawiki/index.php/Docker
 +
|-
 +
|연동 라이브러리 설치
 +
|서버와 채널을 연동할 라이브러리를 설치한다.
 
|pip install channels_redis
 
|pip install channels_redis
 
|-
 
|-
|settings.py 설정
+
|세팅
|ASGI 아래 추가하자.
+
|settings.py 안에 추가.
 
|<syntaxhighlight lang="python">
 
|<syntaxhighlight lang="python">
ASGI_APPLICATION = 'routing.application' # routing.py 파일의 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>을 추가해준다.
 +
|}
 +
==기존 컨슈머 변형==
 +
위에서 작성한 컨슈머는 중간 확인을 위한 동기식 함수인데, 비동기식으로 다시 쓰이면 성능이 좋아진다. 아래와 같이 변형하자.<syntaxhighlight lang="python">
 +
import json
 +
 +
from channels.generic.websocket import AsyncWebsocketConsumer
 +
 +
 +
class ChatConsumer(AsyncWebsocketConsumer):
 +
    async 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
 +
        await self.channel_layer.group_add(self.room_group_name, self.channel_name)
 +
 +
        await self.accept()
 +
 +
    async def disconnect(self, close_code):
 +
        # Leave room group
 +
        await self.channel_layer.group_discard(self.room_group_name, self.channel_name)
 +
 +
    # Receive message from WebSocket
 +
    async def receive(self, text_data):
 +
        text_data_json = json.loads(text_data)
 +
        message = text_data_json["message"]
 +
 +
        # Send message to room group
 +
        await self.channel_layer.group_send(
 +
            self.room_group_name, {"type": "chat_message", "message": message}
 +
        )
 +
 +
    # Receive message from room group
 +
    async def chat_message(self, event):
 +
        message = event["message"]
 +
 +
        # Send message to WebSocket
 +
        await self.send(text_data=json.dumps({"message": message}))
 
</syntaxhighlight>
 
</syntaxhighlight>
|}
 
  
 +
= 서버에 반영 =
 +
튜토리얼엔 나오지 않지만(공식문서 한참 뒤에 나온다..), 서버에 반영하게 되면 ws/ 경로에 대하여 새로운 설정을 해주어야 한다.(필자는 이걸 몰라서 이것저것 건드리다가 sql 날려먹었다.. 젠장)
  
== 개요 ==
+
일반적으로 WSGI 통신을 gunicorn으로 처리한다면, ASGI통신은 daphne으로 처리한다.(daphne에서도 wsgi통신이 가능하지만, 보안상 권장되진 않는다.)
레디스 서버를 이용한 실시간 채팅 구현
 
===사전설정===
 
 
{| class="wikitable"
 
{| class="wikitable"
 
!과정
 
!과정
289번째 줄: 355번째 줄:
 
!방법
 
!방법
 
|-
 
|-
| 서버 설치
+
| 설치
 +
|daphne은 channels를 설치하면 자동으로 설치가 된다.
 
|
 
|
|apt-get install redis-server
 
 
|-
 
|-
|서버 테스트
+
|확인
|서버가 잘 설치되었는지 테스트.
+
|실행 명령은 우측과 같다.(asgi의 경로를 지정해주면 된다.)
서버 구동에 대한 메시지가 뜬다.
+
 
|redis-server
+
포트번호는 필요에 따라 지정하면 된다.
 +
 
 +
해당 포트로 접속해보면 사이트와 채팅이 잘 작동한다.
 +
|<code>daphne -b 0.0.0.0 -p 8001 config.asgi:application</code>
 
|-
 
|-
|채널 라이브러리 설치
+
|서버에서 실행
|장고에서 사용할 채널 라이브러리를 설치한다.
+
|서비스를 위해선 백그라운드로 진행해주어야 한다.
|pip install -U channels
+
실행 여부는 <code>ps -ef | grep daphne</code>로 확인해보자. 실행이 안되면 nohup 떼서 에러메시지 확인.
 +
|<code>nohup daphne -b 0.0.0.0 -p 8001 config.asgi:application &</code>
 
|-
 
|-
|연동 라이브러리 설치
+
|웹서버에 반영
|서버와 채널을 연동할 라이브러리를 설치한다.
+
|/static을 반영하듯, /ws로 시작되는 주소는 다핀으로 넘겨준다.
|pip install channels_redis
+
이후 관련 서비스들을 재시작하면 설정들이 반영된다.
|-
 
|앱을 만든다.
 
|
 
|django-admin startapp chat
 
|-
 
|앱 등록
 
|settings.py에 추가.
 
|INSTALLED_APPS 가장 위에 'channels', 'chat'을 맨 위에 추가해준다.
 
다른 서드파티 앱과 충돌할 수 있어 가장 처음에 둔다.
 
|-
 
|세팅
 
|settings.py 안에 추가.
 
|<syntaxhighlight lang="python">
 
ASGI_APPLICATION = 'config.routhing.application'
 
  
CHANNEL_LAYERS = {
+
채팅도 물론 성공적...!
    'default':{
+
|<syntaxhighlight lang="bash">
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
+
location /ws {
        'CONFIG': {
+
                proxy_pass http://localhost:8001;
            "hosts": [('127.0.0.1', 6379)],
+
                proxy_http_version 1.1;
 +
                proxy_set_header Upgrade $http_upgrade;
 +
                proxy_set_header Connection "upgrade";
 
         }
 
         }
    }
+
</syntaxhighlight>
}
 
 
 
os.environ["DJANGO_ALLOW_ASYNC_UNDAFE"] = "true"
 
</syntaxhighlight>을 추가해준다.(대강 스테틱 설정 아래면 적당)
 
 
|}
 
|}
==모델작성==
+
웹소켓에서도 https 인증을 적용할 수 있다.([https://victorydntmd.tistory.com/265 추후에 반영해보자.])  
chat 앱의 models.py 작성<syntaxhighlight lang="python">
 
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
 
</syntaxhighlight>chat앱의 admin.py 작성<syntaxhighlight lang="python">
 
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']
 
</syntaxhighlight>대화모델 작성(보통 친구모델 등에 작성된다.)(근데 그냥 chat 앱 안에 작성하면 안되나...?)<syntaxhighlight lang="python">
 
...
 
from .chat.models import Room, Message
 
  
class chat_connection(models.Model):
+
=== The app module <module '앱이름' (<_frozen_importlib_external._NamespaceLoader object at 0x7f2faed9a8f0>)> has multiple filesystem locations (['/./앱경로', '/앱경로']); you must configure this app with an AppConfig subclass with a 'path' class attribute. ===
    target = models.ForeignKey(settings.AUTH_USER_MODEL, blank=True, on_delete=models.CASCADE) # 상대방
+
생각지도 못한 문제점이었다... 앱은 하나의 모듈처리가 되어 디렉토리 안에 __init__.py가 담기는데, 이게 없는 경우 경로를 제대로 찾지 못해 발생하는 에러이다. 앱 디렉토리 안에 __init__.py를 제대로 넣어주면 해결됨.
    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 + "채팅"  # 사용자명을 반환한다.
 
</syntaxhighlight>
 
==뷰 작성==
 
<syntaxhighlight lang="python">
 
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)
 
  
   
+
=== asgi.py ===
</syntaxhighlight>
+
{| class="wikitable"
==url 작성==
+
!항목
config의 url을 넘김 처리하고..
+
!설명
 +
|-
 +
| ProtocolTypeRouter
 +
|연결의 종류를 파악하여 처리. http로 넘길지, 웹소켓 처리로 넘길지.
  
chat앱 안의 urls.py를 작성한다.<syntaxhighlight lang="python">
+
* http 연결일 경우 보통 django_asgi_app<code>(get_asgi_application())</code> 가 실행된다.
from .views import *
+
* websocket 연결(ws:// 혹은 wss://)일 경우 <code>AuthMiddlewareStack</code>으로 앱별로 <code>routing.py</code>에 적어놓은 코드로 라우팅 한다.
 +
|-
 +
|
 +
|
 +
*
 +
|}
  
app_name= 'chat'
+
=== routing.py ===
 +
들어온 경로와 컨슈머를 연결해준다. urls.py와 view.py의 관계.
  
urlpatterns = [
+
다음과 같은 형태로 컨슈머를 연결한다.<syntaxhighlight lang="python">
     path('', chat_list, name='chat_list'),
+
websocket_urlpatterns = [
    paht('<str:room_id', room, name='room'),
+
     re_path(r'ws/notification/(?P<center_name>\w+)/$', consumers.NotificationConsumer.as_asgi()),
 
]
 
]
 
</syntaxhighlight>
 
</syntaxhighlight>
==탬플릿 작성 ==
 
chat 앱 안의 templates 폴더를 만든 다음 넣는다.<syntaxhighlight lang="html+django">
 
<!--확장코드-->
 
 
<!--들어가야 할 것들-->
 
<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>
 
</syntaxhighlight>
 
 
[[분류:장고 웹소켓]]
 
[[분류:장고 웹소켓]]

2023년 1월 18일 (수) 13:30 기준 최신판

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 채널 레이어 구현[편집 | 원본 편집]

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

3.1 레디스[편집 | 원본 편집]

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

꽤 오래된 버전인 5를 사용하는데, 아마도 이는.. 5에서 처음으로 consumer groups이 구현된 버전인데, 이걸로 구현했기 때문이 아닐까 싶다.(최신버전에서도 될진.. 검증이 필요하다.)

3.1.1 사전설정[편집 | 원본 편집]

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

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

네이티브로 설치하는 게 아니라 도커를 사용할 거라면 아래로 넘어가자.

단순 apt-get install redis로 설치해 5 이상의 버전이 되어도 작동한다.

Redis 문서 참조.
도커 도커가 설치되어 있다면 아래 명령으로 바로 설치, 서비스가 시작된다.

docker run -p 6379:6379 -d redis:5

아래 문서를 통해 도커 설치를 참고하자.

http://id8436.iptime.org:8080/mediawiki/index.php/Docker

연동 라이브러리 설치 서버와 채널을 연동할 라이브러리를 설치한다. 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)],
        },
    },
}
을 추가해준다.

3.2 기존 컨슈머 변형[편집 | 원본 편집]

위에서 작성한 컨슈머는 중간 확인을 위한 동기식 함수인데, 비동기식으로 다시 쓰이면 성능이 좋아진다. 아래와 같이 변형하자.

import json

from channels.generic.websocket import AsyncWebsocketConsumer


class ChatConsumer(AsyncWebsocketConsumer):
    async 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
        await self.channel_layer.group_add(self.room_group_name, self.channel_name)

        await self.accept()

    async def disconnect(self, close_code):
        # Leave room group
        await self.channel_layer.group_discard(self.room_group_name, self.channel_name)

    # Receive message from WebSocket
    async def receive(self, text_data):
        text_data_json = json.loads(text_data)
        message = text_data_json["message"]

        # Send message to room group
        await self.channel_layer.group_send(
            self.room_group_name, {"type": "chat_message", "message": message}
        )

    # Receive message from room group
    async def chat_message(self, event):
        message = event["message"]

        # Send message to WebSocket
        await self.send(text_data=json.dumps({"message": message}))

4 서버에 반영[편집 | 원본 편집]

튜토리얼엔 나오지 않지만(공식문서 한참 뒤에 나온다..), 서버에 반영하게 되면 ws/ 경로에 대하여 새로운 설정을 해주어야 한다.(필자는 이걸 몰라서 이것저것 건드리다가 sql 날려먹었다.. 젠장)

일반적으로 WSGI 통신을 gunicorn으로 처리한다면, ASGI통신은 daphne으로 처리한다.(daphne에서도 wsgi통신이 가능하지만, 보안상 권장되진 않는다.)

과정 설명 방법
설치 daphne은 channels를 설치하면 자동으로 설치가 된다.
확인 실행 명령은 우측과 같다.(asgi의 경로를 지정해주면 된다.)

포트번호는 필요에 따라 지정하면 된다.

해당 포트로 접속해보면 사이트와 채팅이 잘 작동한다.

daphne -b 0.0.0.0 -p 8001 config.asgi:application
서버에서 실행 서비스를 위해선 백그라운드로 진행해주어야 한다.

실행 여부는 ps -ef | grep daphne로 확인해보자. 실행이 안되면 nohup 떼서 에러메시지 확인.

nohup daphne -b 0.0.0.0 -p 8001 config.asgi:application &
웹서버에 반영 /static을 반영하듯, /ws로 시작되는 주소는 다핀으로 넘겨준다.

이후 관련 서비스들을 재시작하면 설정들이 반영된다.

채팅도 물론 성공적...!

location /ws {
                proxy_pass http://localhost:8001;
                proxy_http_version 1.1;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
        }

웹소켓에서도 https 인증을 적용할 수 있다.(추후에 반영해보자.)

4.1 관련에러[편집 | 원본 편집]

4.1.1 The app module <module '앱이름' (<_frozen_importlib_external._NamespaceLoader object at 0x7f2faed9a8f0>)> has multiple filesystem locations (['/./앱경로', '/앱경로']); you must configure this app with an AppConfig subclass with a 'path' class attribute.[편집 | 원본 편집]

생각지도 못한 문제점이었다... 앱은 하나의 모듈처리가 되어 디렉토리 안에 __init__.py가 담기는데, 이게 없는 경우 경로를 제대로 찾지 못해 발생하는 에러이다. 앱 디렉토리 안에 __init__.py를 제대로 넣어주면 해결됨.

5 마무리. 각 요소들이 어떤 역할을 하는지.[편집 | 원본 편집]

순서대로 어떤 경로를 거쳐 작동하는지 살펴보자.

5.1 asgi.py[편집 | 원본 편집]

항목 설명
ProtocolTypeRouter 연결의 종류를 파악하여 처리. http로 넘길지, 웹소켓 처리로 넘길지.
  • http 연결일 경우 보통 django_asgi_app(get_asgi_application()) 가 실행된다.
  • websocket 연결(ws:// 혹은 wss://)일 경우 AuthMiddlewareStack으로 앱별로 routing.py에 적어놓은 코드로 라우팅 한다.

5.2 routing.py[편집 | 원본 편집]

들어온 경로와 컨슈머를 연결해준다. urls.py와 view.py의 관계.

다음과 같은 형태로 컨슈머를 연결한다.

websocket_urlpatterns = [
    re_path(r'ws/notification/(?P<center_name>\w+)/$', consumers.NotificationConsumer.as_asgi()),
]

5.3 [컨슈머는 컨슈머 문서로 옮김][편집 | 원본 편집]