ASGI (Asynchronous Server Gateway Interface) & WSGI (Web Server Gateway Interface)
웹 애플리케이션과 웹 서버간에 표준화된 통신 인터페이스를 제공하는 통신 프로토콜인데, ASGI는 비동기적인 프로토콜이며 WSGI는 동기적인 프로토콜입니다.
- 동기적: 작업이 끝날때까지 요청을 처리하지 않고 대기해서 I/O 작업이 있는 애플리케이션에서 성능이 저하될 수 있음 -> 실시간 기능을 지원하기 어려움
- 비동기적: 작업이 끝날때까지 요청을 블로킹하지 않고 다른 작업을 처리하여 더 빠른 응답 시간과 더 높은 동시성 제공 -> 실시간 기능을 구현할 수 있음 , 따라서 채팅, 실시간 업데이트 , 실시간 게임과 같은 애플리케이션 개발에 좋음
장고에서 ASGI 사용해서 채팅 기능 구현하기
장고 3.0부터 ASGI를 공식적으로 지원해서 비동기처리 및 실시간 기능을 구현할 수 있게 됨
<구현 순서>
- 필요한 패키지 설치 및 설정
- WebSocket 컨슈머 작성
- WebSocket 라우팅 설정
- WebSocket 클라이언트 구현
- Redis 사용해서 채널 레이어 활성화
1. 사전 준비
- 프로젝트에 chat 앱 생성 & 설정에 추가해주기
django-admin startapp chat
INSTALLED_APPS = [
..
'chat',
]
- 채팅 화면 보여줄 화면과 Url 및 view 작성
1) chat> views.py
여기 예제에서 course_id는 구현해놓은 다른 앱에서 가져와서 사용했는데,
채팅창을 띄워줄 화면을 간단하게 구성하는 view,url, templates 한쌍을 만들어주세요.
from django.contrib.auth.decorators import login_required
from django.http import HttpResponseForbidden
from django.shortcuts import render
# Create your views here.
@login_required
def course_chat_room(request, course_id):
try:
course = request.user.courses_joined.get(id=course_id)
except:
return HttpResponseForbidden()
return render(request, 'chat/room.html', {'course':course})
2) chat > urls.py
course_id = 1 이면, 해당 코스의 채팅창이 열립니다. (코스별로 새로운 채팅창 열리도록 설정)
from django.urls import path
from . import views
app_name='chat'
urlpatterns =[
path('room/<int:course_id>/', views.course_chat_room, name='course_chat_room'),
]
3) chat > templates > chat > room.html
{% extends 'base.html' %}
{% block title %} chat room for {{ course.title}} {% endblock %}
{% block content %}
<div id="chat">
</div>
<div id="chat-input">
<input type="text" id="chat-message-input">
<input type="submit" value="send" id="chat-message-submit">
</div>
{% endblock %}
{% block include_js %}
// 추후 채울 예정
{% endblock %}
{% block domready %}
// 추후 채울 예정
{% endblock %}
[빈 채팅창 생성 완료 ]
2. 채팅서버 구축 준비
*웹소켓이란
WebSockets는 서버와 클라이언트 간에 실시간 양방향 통신을 제공합니다.
웹 애플리케이션과 웹 서버간의 지속적인 연결을 통해 데이터를 양방향으로 전송할 수 있게 해주는 기술입니다.
WebSocket은 전화상태처럼 끊어지지 않고 상대가 말할때까지 계속 열려있는 상태를 유지하여 서버 또는 클라이언트에서 언제든 데이터를 보낼 수 있습니다.
특징
- 실시간 통신
- 낮은 오버헤드: HTTP와 비교하여 상대적으로 적은 오버헤드를 가지며, 작은 헤더 크기로 데이터를 전송할 수 있음
- 지속적인 연결: 클라이언트가 서버에 요청을 보내면 서버가 응답을 보내고 연결을 끊는 HTTP와 달리 지속적으로 연결을 유지함
- 양방향 통신: 서버와 클라이언트간에 양방향으로 데이터를 보낼 수 있으므로, 클라이언트가 서버에 요청을 보내면 서버는 실시간으로 응답할 수 있음
- 일반적으로 ws:// 또는 wss:// (암호화된 연결) 으로 시작
따라서 웹소켓을 사용해서 채팅서버를 구현해보겠습니다.
1) channel 라이브러리 설치 및 셋팅
장고에서 비동기 및 실시간 기능 구현을 할 수 있도록 도와주는 라이브러리로 웹소켓 통신을 비롯한 다양한 비동기 작업을 처리할 수 있습니다. (channels와 함께 daphne 웹서버 설치)
- daphne: 장고 애플리케이션을 위한 ASGI 웹서버로 웹소켓 지원함
pip install -U 'channels[daphne]'
# Channels가 여기 추가되면, runserver 명령을 대체해서 표준 장고 개발 서버 대신 channels 개발 서버를 제공하며
# HTTP 요청은 이전과 동일하게 처리되지만 Channels를 통해 라우팅 됨
INSTALLED_APPS = [
'daphne',
'channels',
...
]
# Channels가 루트 라우팅 구성을 찾을 수 있도록 함
ASGI_APPLICATION = 'educa.asgi.application'
2) 프로젝트 메인 폴더의 asgi.py 파일 편집
- ProtocolTypeRouter 클래스를 사용하여 http, websocket 을 위한 프로토콜을 추가
- websocket에 정의된 URLRouter는 아래에서 생성할 예정
import os
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from django.core.asgi import get_asgi_application
import chat.routing
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'educa.settings')
django_asgi_app = get_asgi_application()
application = ProtocolTypeRouter({
'http':django_asgi_app,
'websocket': AuthMiddlewareStack(
URLRouter(chat.routing.websocket_urlpatterns)
),
})
3) 개발서버 실행해서 Channels 개발서버 사용중인지 확인하기
- Starting ASGI/Daphne version development server at 주소 문장이 출력되면 channels 개발 서버 사용하는것
3. 채팅 서버 구축
구축 단계 별 역할
- 컨슈머 설정: webSocket 연결을 처리하고 WebSocket을 통해 받은 메시지를 다시 전송
- 라우팅 구성: 채팅 컨슈머의 URL 라우팅 구성
- 웹소켓 클라이언트 구현: 브라우저에서 WebSocket에 연결하고 JavaScript를 사용해서 메시지를 보내거나 받을 수 있도록 구현
- 채널 레이어 활성화: 다른 인스턴스간에 통신할 수 있도록 구성 (Redis 사용) , 하나의 채팅창에 여러명이 메시지를 주고 받을 수 있도록
1) 컨슈머 작성
컨슈머는 비동기 애플리케이션에서 장고 view의 역할을 함 (로직 처리)
- AsyncWebSocketConsumer 클래스 상속(비동기소켓)
- 함수 앞에 async 붙여서 비동기 함수임을 명시
- 함수 안에 클래스의 함수 실행 앞에 await을 붙여서 비동기적으로 실행
chat > consumers.py
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import AsyncWebsocketConsumer
from django.utils import timezone
class ChatConsumer(AsyncWebsocketConsumer):
# 새 연결이 수신되었을 때 호출 됨
async def connect(self):
#연결 수락
await self.accept()
# 소켓이 닫힐 때 호출
async def disconnect(self, close_code):
#추후 구현
pass
# 클라이언트에서 데이터를 수신할 때 마다 호출됨
async def receive(self, text_data):
# 수신된 데이터를 json으로 처리
text_data_json = json.loads(text_data)
# message 로 받아올 것을 미리 규약
message = text_data_json['message']
# 메시지를 에코해서 다시 WebSocket으로 보내줌
await self.send(text_data=json.dumps({'message':message}))
여기서 receive 함수가 헷갈렸는데, 기존에 뷰에서는 서버에서 클라이언트에 응답을 주는 로직이었는데 여기서는 반대로 클라이언트에서 주는 데이터를 받아와서 다시 웹소켓에 전달하는 로직으로 받아올 때 로직을 미리 정해 둔 것
2) 라우팅 구성
장고 앱의 urls.py 와 비슷함
위에 작성한 ChatConsumer 클래스를 아래 라우팅 파일에서 정의한 URL 패턴과 매핑합니다.
-> 준비단계에서 작성한 asgi.py 파일에서 해당 코드줄에 사용됨
'websocket': AuthMiddlewareStack(URLRouter(chat.routing.websocket_urlpatterns)
chat > routing.py
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/room/(?P<course_id>\d+)/$',
consumers.ChatConsumer.as_asgi()),
]
3) 클라이언트 웹소켓 작성
- window.location.host : 브라우저의 현재 위치
- onmessage: 웹소켓을 통해 데이터가 수신될 때 발생
- onclose: 웹소켓과의 연결이 닫힐 때 발생
- send: 웹소켓을 통해 json 콘텐츠 전송
chat > room.html
{% block include_js %}
{{ course.id|json_script:'course-id' }}
{% endblock %}
{% block domready %}
const couseId = JSON.parse(
document.getElementById('course-id').textContent
);
// courseid 넣은 websocket url 구성
const url = 'ws://' + window.location.host +
'/ws/chat/room/' + courseId + '/';
// 해당 url에 연결되는 웹소켓 생성
const chatSocket = new WebSocket(url);
// 서버에서 웹소켓으로 메시지 받기
chatSocket.onmessage = function(event){
const data = JSON.parse(event.data);
const chat = document.getElementById('chat');
// 화면에 메시지 출력하기 위해 html 요소 만들기
chat.innerHTML += '<div class="message">' +
data.message + '</div>';
};
// 클라이언트에서 웹소켓으로 메시지 보내기
const input = document.getElementById('chat-message-input');
const submitButton =document.getElementById('chat-message-submit');
submitButton.addEventListener('click', function(event) {
if(message) {
//send message in json format
chatSocket.send(JSON.stringify({'message':message}));
// 작성 창 지워주기
input.value = '';
input.focus();
}
});
// 소켓 닫힐 때 콘솔로그 출력
chatSocket.onclose = function(event){
console.error('chat socket closed');
};
{% endblock %}
웹소켓이 잘 연결되었다면, 해당 url에 접속했을때 콘솔에 WebSocket 관련해서 두가지가 출력되어야함
위 예제에서 테스트할 url: http://127.0.0.1:8000/chat/room/1/
4) 채널 레이어 활성화
웹소켓을 통해 클라이언트/서버간의 커뮤니케이션을 구현했는데, 다른 클라이언트와 메시지를 공유할 수 없다.
해당 웹소켓에 다른 클라이언트가 접속해서 메시지를 공유할수있도록 하려면 채널 레이어를 활성화 해줘야한다.
channels-redis 패키지 설치
pip install channels-redis
settings.py 설정
# redis 사용한 channel layer
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
도커 실행
docker run -it --rm --name redis -p 6379:6379 redis
5) 컨슈머 코드 업데이트
- channel_layer.group_add: 코스 id로 그룹을 만들고 채널에 그룹 추가
- channel_layer.group_send: 따라서 id=1 인 코스방에 들어오는 클라이언트는 채널그룹이 같고 같은 메시지를 공유함
import json
from channels.generic.websocket import AsyncWebsocketConsumer
from django.utils import timezone
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
# 채널 그룹 만들기
self.user = self.scope['user']
self.id = self.scope['url_route']['kwargs']['course_id']
self.room_group_name = f'chat_{self.id}'
# 현재 채널을 그룹에 추가 : group_add
# ChatConsumer는 동기적인 웹소켓이고, 채널 레이어에서는 비동기 메서드 작업을 해주어야함
await self.channel_layer.group_add(
self.room_group_name,
self.channel_name
)
#연결 수락
await self.accept()
async def disconnect(self, close_code):
#그룹에서 나가기
await self.channel_layer.group_discard(
self.room_group_name,
self.channel_name
)
#websocket에서 메시지 수신
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
now = timezone.now()
# 그룹에 메시지 보내기
await self.channel_layer.group_send(
self.room_group_name,
{
# 해당 이벤트를 받는 컨슈머에서 실행되어야 할 메서드의 이름과 일치해야함
# 컨슈머에 동일한 이름의 메서드를 구현하면 해당 이벤트 타입이 수신될때마다 실행됨
'type':'chat_message',
'message': message,
'user':self.user.username,
'datetime': now.isoformat(),
}
)
#chat_message 함수에서 실행
#self.send(text_data=json.dumps({'message':message}))
async def chat_message(self, event):
# webSocket으로 메시지 보내기
await self.send(text_data=json.dumps(event))
'Today I Learned > django' 카테고리의 다른 글
장고 (djagno) Rest framework tutorial #2 데이터 보내기(POST) (0) | 2023.09.11 |
---|---|
장고(django) 회원가입 (UserCreationForm) (0) | 2023.09.07 |
장고 (djagno) Rest framework tutorial #1 데이터 가져오기 (GET) (0) | 2023.09.05 |
장고 (django) 캐시 사용하기 (redis) (0) | 2023.09.01 |
장고 (django) 캐시 사용하기 (memcached) (0) | 2023.08.31 |