최근 정처기 자격증을 준비하며
WebSocket
통신에 관심을 가졌고, 이전 프로젝트 이후redis
를 적용해보고 싶다는 막연한 생각으로 실시간 채팅 서비스를 배워보고자 간단한 튜토리얼을 진행 해보았습니다.
📌 상세과정은 Django 튜토리얼 참고
- Channels는 Django에서 비동기 통신 뷰를 실현하는 기능
- HTTP, HTTPS, WebSocket, MQTT 등 다양한 프로토콜을 지원
- Channels를 활용하여 WebSocket을 사용한 채팅 앱을 만들 수 있습니다.
- 서버와 클라이언트 간 메시지 교환을 위한 프로토콜
- 양방향 통신 : 기존 HTTP 통신은 클라이언트-서버가
요청-응답
하는 단방향 통신- 실시간 네트워킹이 가능
- 동작과정
1. 브라우저에서 WebSocket 서버로 요청이 들어오면, HTTP -> WS로 프로토콜 전환 후 승인되었음을 반환 :Opening Handshake
2. 데이터는 메시지 단위로 전달 : 메시지 = 여러 프레임(작은 헤더 + payload)의 모임
3. WebSocket 서버에서 close를 반환하면, 브라우저에서 확인 후 반응 :Closing Handshake
$ pip install django
$ pip install channels
INSTALLED_APPS = [
'channels', # 다른 앱과의 충돌 방지를 위해 맨 위에 추가
...
]
import os
from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE' ,'<project이름>.settings')
application = ProtocolTypeRouter ({
'http':get_asgi_application()
})
# settings.py
...
ASGI_APPLICATION = '<project이름>.asgi.application'
chat
이라는 django app 생성 후, INSTALLED_APPS에 추가$ python3 manage.py startapp 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')
from django.urls import path, include
from django.contrib import admin
urlpatterns = [
path('chat/', include('chat.urls')),
path('admin/', admin.site.urls),
]
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
<!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 = (e) => {
const data = JSON.parse(e.data);
document.querySelector('#chat-log').value += (data.message + '\n');
};
chatSocket.onclose = (e) => {
console.error('Chat socket closed unexpectedly');
};
document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').addEventListener("keyup",(e) => {
if (e.key === 13) { // enter, return
document.querySelector('#chat-message-submit').click();
}
});
document.querySelector('#chat-message-submit').addEventListener("click",(e) => {
const messageInputDom = document.querySelector('#chat-message-input');
const message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message
}));
messageInputDom.value = '';
});
</script>
</body>
</html>
# views.py
from django.shortcuts import render
def index(request):
return render(request, 'chat/index.html')
# room.html을 보여주는 함수 추가
def room(request, room_name):
return render(request, "chat/room.html", {"room_name": room_name})
# urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('<str:room_name>/', views.room, name='room'),
]
WebSocket connection to 'ws://127.0.0.1:8000/ws/chat/<채팅방이름>/' failed:
Chat socket closed unexpectedly
root routing configuration
에서 consumer
를 찾은 후 이벤트 처리를 위한 함수들을 호출하기에 consumer 작성 필요# consumers.py
from channels.generic.websocket import AsyncWebsocketConsumer
import json
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = f'chat_{self.room_name}'
# 그룹에 join
# send 등 과 같은 동기적인 함수를 비동기적으로 사용하기 위해서는 async_to_sync 로 감싸줘야함
# Join room group
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
# WebSocket 연결
await self.accept()
async def disconnect(self, close_code):
# 그룹에서 Leave
# Leave room group
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
# WebSocket 에게 메세지 receive
# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
# room group 에게 메세지 send
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
# room group 에서 메세지 receive
# Receive message from room group
async def chat_message(self, event):
message = event['message']
# WebSocket 에게 메세지 전송
# Send message to WebSocket
await self.send(text_data=json.dumps({
'message': message
}))
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer.as_asgi()),
]
re_path : 내부 라우터가 미들웨어에 감싸져 있을 경우, path()가 제대로 동작하지 않을 수 있기에 사전 방지하고자 사용
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from chat.routing import websocket_urlpatterns
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', '<project이름>.settings')
django_asgi_app = get_asgi_application()
# 클라이언트와 Channels 개발 서버가 연결 될 때, 어느 protocol 타입의 연결인지
application = ProtocolTypeRouter({
'http': django_asgi_app,
# (http->django views is added by default)
# 만약에 websocket protocol 이라면, AuthMiddlewareStack
'websocket': AllowedHostsOriginValidator(
AuthMiddlewareStack(URLRouter(websocket_urlpatterns))
# URLRouter 로 연결, 소비자의 라우트 연결 HTTP path를 조사
),
})
brew install redis
redis-server
// background로 실행
brew services start redis
$ pip install channels_redis
ASGI_APPLICATION
밑에 설정을 추가...
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
$ 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'} ---> 연결 성공임을 확인
>>> exit()
Error 상황
: ValueError: No route found for path 'ws/chat/test/'.
--> Web socket connection error -> 웹소켓 라우팅 경로 문제
: routing.py에서 경로 수정
# before
from django.urls import re_path
from . import consumers
>
websocket_urlpatterns = [
re_path('ws/chat/<room_name>', consumers.ChatConsumer.as_asgi()),
]
# after
from django.urls import re_path
from . import consumers
>
websocket_urlpatterns = [
re_path(r'^ws/chat/(?P<room_name>[^/]+)/$', consumers.ChatConsumer.as_asgi()),
]
📌 전체 코드는 Github에 있습니다.
이번 튜토리얼을 바탕으로 웹 소켓 통신에 대해 공부할 수 있었고, 튜토리얼을 진행하며 100%의 이해는 되지 않았지만, 블로그에 정리함으로써 좀 더 이해할 수 있었습니다. 이후에는 이를 응용한 토이 프로젝트를 기획하고 개발 할 수 있도록 더 공부해보겠습니다.
틀린부분, 의문점 등 질문은 언제나 환영합니다 :D
유익한 글이었습니다.