"장고 실시간 채팅(Channels와 redis server 이용)"의 두 판 사이의 차이
(→참가자 구현) |
(→개요) |
||
260번째 줄: | 260번째 줄: | ||
같은 브라우저를 띄워두고 한쪽에서 채팅을 치면 다른 쪽에서 안뜨는데, 다른 브라우저에선 안뜬다. 접속한 모두에게 반영될 수 있도록 채널레이어를 구현한다. | 같은 브라우저를 띄워두고 한쪽에서 채팅을 치면 다른 쪽에서 안뜨는데, 다른 브라우저에선 안뜬다. 접속한 모두에게 반영될 수 있도록 채널레이어를 구현한다. | ||
+ | = 채널 레이어 구현 = | ||
=== 체널레이어 구현 === | === 체널레이어 구현 === | ||
+ | consumer 인스턴스는 자동으로 유일한 channel name을 구성하기 때문에 서로 소통하려면 layer가 필요하다. | ||
{| class="wikitable" | {| class="wikitable" | ||
!과정 | !과정 | ||
288번째 줄: | 290번째 줄: | ||
== 개요 == | == 개요 == | ||
− | 레디스 서버를 이용한 실시간 채팅 구현 | + | 레디스 서버를 이용한 실시간 채팅 구현. |
+ | |||
+ | 꽤 오래된 버전인 5를 사용하는데, 아마도 이는.. 5에서 처음으로 consumer groups이 구현된 버전인데, 이걸로 구현했기 때문이 아닐까 싶다. | ||
===사전설정=== | ===사전설정=== | ||
{| class="wikitable" | {| class="wikitable" | ||
296번째 줄: | 300번째 줄: | ||
|- | |- | ||
| 서버 설치 | | 서버 설치 | ||
− | | | + | |설치 후 딱히 옵션을 변경할 내용은 없다. |
− | | | + | |
+ | 어차피 로컬에서 모든 작동이 수행되기 때문에. | ||
+ | |[http://id8436.iptime.org:8080/mediawiki/index.php/Radis Redis] 문서 참조. | ||
|- | |- | ||
|서버 테스트 | |서버 테스트 | ||
303번째 줄: | 309번째 줄: | ||
서버 구동에 대한 메시지가 뜬다. | 서버 구동에 대한 메시지가 뜬다. | ||
|redis-server | |redis-server | ||
− | |||
− | |||
− | |||
− | |||
|- | |- | ||
|연동 라이브러리 설치 | |연동 라이브러리 설치 | ||
|서버와 채널을 연동할 라이브러리를 설치한다. | |서버와 채널을 연동할 라이브러리를 설치한다. | ||
|pip install channels_redis | |pip install channels_redis | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
|- | |- | ||
|세팅 | |세팅 | ||
|settings.py 안에 추가. | |settings.py 안에 추가. | ||
|<syntaxhighlight lang="python"> | |<syntaxhighlight lang="python"> | ||
− | ASGI_APPLICATION = | + | ASGI_APPLICATION = "mysite.asgi.application" # 이부분은 채널을 설치할 때 작성한 부분. |
− | |||
CHANNEL_LAYERS = { | CHANNEL_LAYERS = { | ||
− | + | "default": { | |
− | + | "BACKEND": "channels_redis.core.RedisChannelLayer", | |
− | + | "CONFIG": { | |
− | "hosts": [( | + | "hosts": [("127.0.0.1", 6379)], |
− | } | + | }, |
− | } | + | }, |
} | } | ||
− | + | </syntaxhighlight>을 추가해준다. | |
− | + | |- | |
− | </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'),
]
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>