Django Channels를 사용한 웹 소켓 사용하기

JeongJun Min·2024년 2월 4일

Techeer

목록 보기
5/9

HTTP 통신 방법

Polling

  • 서버에 일정한 주기로 요청을 보내는 방법
  • 예) 과제 다했어? -> 아직이야, 잠시만
  • (5초 뒤) 과제 다했어? -> 자, 여기
  • 부하가 크다는 단점

Long Polling

  • 서버에 일정한 주기로 요청을 보내지만 특정 이벤트가 발생했을 때 응답을 하는 방법
  • 응답을 실시간으로 받기 위해 대기
  • 예) 과제 다했어? -> 잠시만............ 자, 여기
  • Polling과 마찬가지로 많은 요청 시 부하가 크다는 단점

Streaming

  • 서버가 요청을 보내면 끊지 않고 계속 응답을 하는 방법
  • 예) 과제 다했어? -> 잠시만...... 아직이야...... 거의 다했어....... 자, 여기

Web Socket

  • 서버와 클라이언트 간 실시간 양방향 통신 방법

  • Handshake : HTTP로 GET 요청 시 ws(웹 소켓 프로토콜)로 업그레이드 된다.

    웹 소켓 요청 헤더
    GET /chat HTTP/1.1
    Host: server.example.com
    Upgrade: websocket
    Connection: Upgrade
    Sec-Websocket-Key: 16-byte nonce, base64 encoded
    Sec-Websocket-Version: 6

    웹 소켓 응답 헤더
    HTTP/1.1 101 Switching Protocols
    Upgrade: websocket
    Connection: Upgrade
    Sec-WebSocket-Accept: 20-byte MD5 hash in base64

  • Handshake 승인 이후 Data Transfer 단계가 진행된다. 데이터는 "메세지"라는 단위로 전달된다.

    메세지: 프레임이라는 통신에서의 가장 작은 단위 데이터들이 모여서 구성된 하나의 논리적인 단위


웹 소켓을 사용하는 곳

  • 채팅 및 멀티플레이어 게임
  • 동시 접속이 가능한 서비스
  • 실시간 주식 차트 등 실시간 양방향 통신이 필요한 응용프로그램

Django Channels

Django에서 HTTP통신을 업그레이드하여 웹 소켓, 채팅 프로토콜, IoT 프로토콜 등을 처리 할 수 있는 프로젝트

Channels 사용하기

  1. channels 설치
pip install channels
  1. INSTALLED_APPS에 새로운 앱 추가하기
settings.py

INSTALLED_APPS = [
	...
    "canvas",
]
  1. ASGI 설정
asgi.py

application = ProtocolTypeRouter(
    {
        "http": get_asgi_application(),
        "websocket": AuthMiddlewareStack(
            URLRouter(websocket_urlpatterns)
        ),
    }
)
  • http 요청 시 동기적인 처리를 수행
  • Websocket 요청 시 AuthMiddlewareStack을 통해 연결에 대한 인증을 처리하고, URL 패턴에 따라 특정 애플리케이션을 실행
  1. urlpatterns 추가
canvas/urls.py

urlpatterns = [
	re_path(r'ws/canvases/(?P<canvas_id>\w+)/$', CanvasConsumer.as_asgi(), name='canvas-edit'),
]
  • re_paths는 json에서 요청 받을 정규표현식으로 ws/canvases/{canvas_id}를 통해 HTTP 요청을 수신할 때 소켓 통신으로 업그레이드 되어 channels에 넘겨준다.
  1. Daphne 추가
Settings.py

INSTALLED_APPS = [
	"Daphne",
    ...
    "canvas",
]

ASGI_APPLICATION  =  'backend.asgi.application'
  • Daphne: django channels를 지원하기 위해 개발된 ASGI 서버, HTTP, HTTP2, WS 프로토콜을 처리

이제 프론트엔드에서 만든 페이지에서 다음과 같이 소켓 연결을 작성한다.

const chatSocket = new WebSocket(
            'ws://'
            + window.location.host
            + '/ws/canvases/'
            + canvasId
            + '/'
        );

consumer끼리 소통하기 위해 Channels layer가 필요하다.
Channels layer는 서버 인스턴스간에 통신을 할 수 있도록 도와준다.
Group: 해당 웹소켓을 하나의 그룹에 추가하고, 그 그룹에 메세지를 보낸다.
Channel: 특정 채널에 메세지를 보내거나 받을 수 있다.
보통 Redis를 백업 저장소로 사용하며 메세지 브로커 역할을 하게 된다.

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'channels_redis.core.RedisChannelLayer',
        'CONFIG': {
            "hosts": [('redis', 6379)],
        },
    },
}

마지막으로 웹 소켓은 consumer가 이벤트를 처리 하는데, HTTP가 요청 되면 view 함수를 호출하듯이 웹 소켓도 consumer에서 여러 함수를 호출하여 처리한다. 프론트엔드에서 웹 소켓을 통해 메세지를 consumer에게 보내고 consumer는 해당 그룹으로 메세지를 전달한다.


import json

from channels.generic.websocket import AsyncWebsocketConsumer
from canvas.models import CanvasMember, Canvas


class CanvasConsumer(AsyncWebsocketConsumer):

    async def connect(self):
        self.canvas_id = self.scope["url_route"]["kwargs"]["canvas_id"]
        self.canvas_group_id = "canvas_group_%s" % self.canvas_id

        await self.channel_layer.group_add(self.canvas_group_id, self.channel_name)  # 그룹으로 참여
        await self.accept()

    async def disconnect(self, close_code):
        await self.channel_layer.group_discard(self.canvas_group_id, self.channel_name)

    async def receive(self, text_data):
        text_data_json = json.loads(text_data)

        if text_data_json.get("type") == "position":
         	user_id = text_data_json['user_id']
        	component_id = text_data_json['component_id']
            position_x = text_data_json['position_x']
            position_y = text_data_json['position_y']

            await self.channel_layer.group_send(
                self.canvas_group_id,
                {
                    'type': 'position',
                    'user_id': user_id,
                    'component_id': component_id,
                    'position_x': position_x,
                    'position_y': position_y
                }
            )


    async def position(self, event):
        user_id = event['user_id']
        component_id = event['component_id']
        position_x = event['position_x']
        position_y = event['position_y']

        await self.send(text_data=json.dumps({
            'type': 'position',
            'user_id': user_id,
            'component_id': component_id,
            'position_x': position_x,
            'position_y': position_y
        }))
  • 웹소켓이 접근할 때 connect 함수가 실행되면 url 파라미터를 넘겨 주어 그룹에 참여 시킨 후 소켓 연결을 허가한다. 반대로 연결이 끊어지면 그룹에서 제거한다.

  • receive함수에서 메세지 전달이 이루어지는데, 위 코드에서 스티커(컴포넌트)를 드래그 앤 드랍 기능을 구현한 것이다.

  • 프론트엔드에서 json형태로 사용자와 스티커, 좌표 값을 받아오고, self.channel_layer.group_send()함수로 그룹에게 메세지를 보낸다.

  • async def position()함수는 그룹으로부터 전달된 메세지를 받을 때 호출되는 메소드이다. event로 전달된 데이터들을 웹 소켓으로 전송한다.

  • 즉, 사용자가 메세지를 보내면 해당 메세지를 그룹에 있는 다른 클라이언트들에게 전송하고, 그룹으로부터 전달된 메세지를 웹 소켓을 통해 해당 클라이언트에게 전송하는 역할을 한다.


참고
Django Channels
[Django] Channels 사용하기 - 1

profile
개발계발

0개의 댓글