Project/AInfo

[WebSocket] WebSocket 이란? (Django Channels)

죵욜이 2025. 3. 14. 11:19

WebSocket

  • 우리가 REST, RESTful 하면서 들어본 HTTP 프로토콜처럼 웹소켓도 프로토콜의 한 종류이다.
  • 웹소켓은 HTTP 프로토콜을 기반으로 하는 양방향 통신 프로토콜 인데
  • 일반적인 HTTP 프로토콜은 한번 요청이 오면 서버에서 처리하고 응답을 하면 일련의 과정이 끝난것이다.
  • 하지만 웹소켓은 한번의 핸드셰이크(초기요청) 이후 통신이 계속 유지되며
더보기

핸드셰이크란?

 

Handshake (HTTP Upgrade)

  • 클라이언트와 서버 간의 연결을 초기화 하는 과정
  • 우리가 익히 알고있는 POST, GET 처럼 OPTION 이라는 메서드로 HTTP 요청을 보낸다
  • 이때 헤더에 Upgrade: websocket과 Connection: Upgrade를 포함하며 HTTP연결을 웹소켓으로 업그레이드 하겠다는 의도를 전한다.
  • 여러 과정을 거치며
    • 서버가 클라이언트의 요청을 받아들임
    • HTTP 101 Switching Protocols 응답을 보냄
    • Upgrade: websocket과 Connection: Upgrade 헤더를 포함하여 웹소켓 프로토콜로 전환한다는 것을 알림
  • 핸드셰이크가 완료되며 양방향 연결이 성립된다
  • 그 이후 HTTP 요청/응답 이 아닌 웹소켓 프로토콜을 통해 실시간으로 데이터를 주고받는다
  • 양쪽 모두 자유롭게 데이터를 실시간으로 전송할 수 있어
  • 채팅과 같은 빠른 상호작용이 필요한 서비스에 매우 적합하다.

 Channels

  • 웹소켓을 쉽게 다룰 수 있도록 도와주는 라이브러리이다.
  • 웹소켓을 그냥 사용하려면 너무 빡샘
  • 왜냐고?
    • Django 는 HTTP 기반이라 웹소켓을 직접 구현하려면 ASGI서버, 비동기 핸들링, 메시지 브로커(Redis같은거) 설정 등을 직접해야함
    • 또한 기본적으로 Django는 동기식 웹 프레임워크 이다
    • 하지만 지금처럼 채팅기능을 구현하려면 웹소켓같은 비동기통신을 처리해야하는데
    • 이런 WebSocket, MQTT, TCP 같은 비동기 통신Channels 를 사용하면 편해짐
    • 동기식 웹 프레임워크인 장고를 채널스만 띡 쓴다고 어떻게 비동기통신이 가능해질까?
      • Channels는 비동기 웹 프로토콜을 처리하기 위해 Django의 기본적인 WSGI 인터페이스를 ASGI(Asynchronous Server Gateway Interface)로 대체합니다.
  • 간단히 말하면 HTTP만 지원하는 장고를 웹소켓이 가능한 장고로 업그레이드 시켜주는 라이브러리 라고 생각하면 된다.

WSGI 와 ASGI

  • WSGI(Web Server Gateway Interface)
    • 동기식 인터페이스 : WSGI는 요청을 처리할 때 하나의 요청이 완전히 처리될 때까지 다른 요청을 기다리게 하는 동기 방식을 사용. 이는 처리할 수 있는 요청의 수가 제한되며, 동시성을 높이기 위해 여러 프로세스나 스레드를 사용해야 함을 의미
    • 장고와의 호환성: Django는 전통적으로 WSGI 표준을 따르는 웹 애플리케이션이며, 이 인터페이스를 통해 웹 서버와 통신
    • 요청-응답 패턴: WSGI는 클라이언트에서 서버로 요청을 보내고 서버가 응답을 반환하는 전형적인 HTTP 요청-응답 패턴을 기반으로 한다. 이 방식은 웹 페이지를 요청하고 응답을 받는 전형적인 웹 애플리케이션에 적합하다.
  • ASGI (Asynchronous Server Gateway Interface)
    • 비동기식 인터페이스 : ASGI는 비동기식 프로그래밍을 지원. 이는 여러 요청을 동시에 처리할 수 있으며, 특히 I/O 작업이 많은 작업에서 높은 효율을 보인다
    • 웹소켓 지원: ASGI는 HTTP 요청뿐만 아니라 웹소켓 같은 양방향 비동기 프로토콜도 지원합니다. 따라서 실시간 통신이 필요한 채팅 애플리케이션에 매우 적합합니다.
    • Django와 Channels: Django는 기본적으로 ASGI를 지원하지 않습니다. Channels 라이브러리를 사용하여 Django 애플리케이션에 ASGI 기능을 추가하고, 비동기 처리 및 웹소켓 통신을 할 수 있습니다.
    • 스케일러빌리티: ASGI는 하나의 이벤트 루프에서 여러 연결을 관리할 수 있기 때문에, 동일한 하드웨어 리소스에서 더 많은 연결을 처리할 수 있습니다. 이는 대규모 실시간 애플리케이션을 구축할 때 중요한 장점입니다.
  • Python 웹 애플리케이션과 웹 서버가 소통하는 방법을 정의 한다고하는데 간단히 말하자면
  • WSGI 는 동기 ASGI 는 비동기
  • 웹소켓을 사용하려면 비동기처리를 해야하는데 WSGI 만쓰면 그걸못함
  • 그래서 Channels 를 사용하면 ASGI 설정을 건드리고 ASGI서버인 Uvicorn, Daphne 같은걸 사용해 비동기 요청을 처리하는거임
  • 그러면 ASGI 서버는 어케띄우냐?
    • 배포환경에선 Nginx 같은 리버스 프록시 서버와 함께 ASGI 서버를 사용한다는데 이거는 나중에 추가기능으로 해야함
    • 원래 runserver 를 사용하면 안됨
    • 그래서 Channels 공식문서에서도 사용하고 웹소켓, 비동기처리에 특화된 Daphne 라는 ASGI서버를 사용해야함
    • 하지만 다행히도 장고3.0 부터 runserver 도 비동기를 지원하기는함 (불안정)
    • 그래서 일단은 그냥 사용

Daphne

  • Django Channels를 위한 ASGI 서버

Redis

  • In-Memory NoSQL 데이터베이스
    • In-Memory : 데이터를 메모리에 저장한다고요
    • NoSQL : Not Only SQL , 관계형 데이터베이스(RDBMS)와 달리 고정된 스키마 없이 데이터를 저장하고 관리하는 데이터베이스를 의미
    • 우리조 정처기쟁이들은 다 알죠? 나만몰라,,,
  • Key-Value 저장 방식
  • 캐시(Cache) 용도로 많이 사용
  • Pub/Sub (발행-구독) 기능
  • Channels 에서는 주로 메시지 브로커 서의 기능을 수행
  • 채널레이어 에서의 데이터전달을 위한 메커니즘을 제공하며
  • 이를통해 컨슈머가 서로 통신할 수 있게 해줌

사용법

초기세팅

mkdir websocketPrac
cd websocketPrac
python -m venv venv
source venv/Scripts/activate
pip install django djangorestframework django-environ langchain-openai
django-admin startproject config .
py manage.py startapp chat

필요패키지 설치

pip install channels channels-redis daphne

config/settings.py → 설치한 라이브러리와 생성한 앱 등록

INSTALLED_APPS = [
    # Third-party apps
    'daphne',     
    'channels',   
    
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
    
    
    # Local apps
    'chat',
]
  • 순서 유의 → 습관적으로 서드파티앱 밑으로 내리면 에러남
  • 공식문서에서도 맨위에 박으라고 함


ASGI  설정

ASGI_APPLICATION = 'config.asgi.application'
  • ASGI 서버를 통해 비동기 웹 요청을 처리할 엔트리 포인트를 지정
  • 즉 장고가 실행되면 config/asgi.py 에있는 application 객체를 ASGI 서버가 사용

채널레이어 설정

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

config/asgi.py

import os

from django.core.asgi import get_asgi_application
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.auth import AuthMiddlewareStack
from chat.routing import websocket_urlpatterns

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'config.settings')

application = ProtocolTypeRouter({
    'http':get_asgi_application(),
    'websocket':AuthMiddlewareStack(
        URLRouter(
            websocket_urlpatterns
        )
    )
})
  • ProtocolTypeRouter: 들어오는 요청의 프로토콜(HTTP, WebSocket 등)을 기반으로 어떤 처리를 할지 결정
  • AuthMiddlewareStack: WebSocket 연결 시 사용자 인증을 처리하는 미들웨어 스택을 제공
  • websocket_urlpatterns : routing.py 에 있는 URL pattern
  • 즉 HTTP 요청은 get_asgi_application 을 통해 기본 HTTP요청 처리기에서 처리
  • 웹소켓 요청은 websocket_urlpatterns 에서 매핑한대로 해당 Consumer 로 라우팅시킴

chat/routing.py

from django.urls import re_path
from . import consumers


websocket_urlpatterns = [
    re_path(r'^ws/chat/$', consumers.ChatConsumer.as_asgi()),
]

 

  • 프론트에서 핸드셰이크 후 요청을
  • ws://localhost:8000/ws/chat/ 이런식으로 보냄
  • 보안 강화하면 http, https 처럼 ws wss 로 강화가능
  • 웹소켓 프로토콜로 온 요청을 컨슈머에서 처리하게 매핑시킴
  • 그냥 urls.py 랑 같다고 보면됨

chat/consumers.py

import json
from channels.generic.websocket import AsyncWebsocketConsumer
from langchain_openai import ChatOpenAI
from langchain_core.output_parsers import StrOutputParser


class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        await self.accept()
    
    async def disconnect(self, close_code):
        pass
    
    async def receive(self, text_data):
        data = json.loads(text_data)
        user_message = data['message']
        
        llm_response = await self.llm_response(user_message)
        
        await self.send(text_data=json.dumps({
            'response': llm_response
        }))
    
    async def llm_response(self, user_message):
        model = ChatOpenAI(model="gpt-4o-mini", temperature=0)
        chain = model | StrOutputParser()
        
        response = chain.invoke(user_message)
        return response

 

  • http 요청은 views.py 에서 처리하는거처럼
  • websocket 요청은 consumer.py 에서 처리
  • 추후에 모델 에서 답변받아오는 llm_response 로직만 utils 에서하든 모듈화해서 가져오면될듯
  • ChatConsumer 클래스는 AsyncWebsocketConsumer 클래스를 상속받고있으며
  • AsyncWebsocketConsumer 는 channels.generic.websocket Channels 라이브러리에서 가져온거임
  • 그말은 뭐다? 편하게 쓸라고 미리 만들어 뒀다는 거임
  • 하나하나 파면서 이해하는거보다 “아~ 그렇구나” 하면 됨
  • 하나 알아야 할건 async 와 await
async 와 await 란?

async와 await의 동작 원리

  • async는 함수 정의 앞에 붙여서 비동기 함수로 만듭니다.
  • await는 비동기 함수 안에서 사용되어, 비동기 함수의 실행을 기다리며 그 함수가 완료되면 결과를 반환합니다.

 

 

  • async : 비동기 함수를 정의하는 데 사용. 비동기 함수는 다른 함수들이 기다리지 않고 동시에 실행될 수 있도록 만들준다.
  • await : 비동기 함수 안에서만 사용할 수 있으며, 비동기 함수가 완료될 때까지 기다리는 역할, 다른 코드가 실행되는 동안 블로킹 없이 작업을 기다리게 해준다

테스트용 JS 코드
<!DOCTYPE html>
<html lang="ko">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>WebSocket 테스트</title>
</head>
<body>
    <h1>제발 제발제발 성공해라 제발제발제발제발</h1>
    <div>
        <input type="text" id="userMessage" placeholder="메시지를 입력하세요">
        <button onclick="sendMessage()">전송</button>
    </div>
    
    <div id="response">
        <!-- GPT-4o 응답을 여기에 표시 -->
    </div>

    <script>
        const socket = new WebSocket("ws://localhost:8000/ws/chat/");
        
        socket.onopen = function () {
            console.log("WebSocket 연결 성공!");
        };

        socket.onmessage = function (event) {
            const data = JSON.parse(event.data);
            console.log("GPT-4o 응답:", data.response);
            document.getElementById("response").innerHTML = "응답: " + data.response;
        };

        socket.onerror = function (error) {
            console.log("WebSocket 에러:", error);
            alert("WebSocket 연결에 문제가 발생했습니다.");
        };

        socket.onclose = function (event) {
            if (event.wasClean) {
                console.log("WebSocket 연결이 정상적으로 종료되었습니다.");
            } else {
                console.log("WebSocket 연결 종료에 문제가 있었습니다.");
            }
        };

        function sendMessage() {
            const message = document.getElementById("userMessage").value;
            if (message) {
                socket.send(JSON.stringify({ message: message }));
                document.getElementById("userMessage").value = ''; // 입력창 비우기
            } else {
                alert("메시지를 입력하세요.");
            }
        }
    </script>
</body>
</html>

 

'Project > AInfo' 카테고리의 다른 글

[Celery] Celery 란?  (0) 2025.03.06
[SMTP] 메일기능 활용  (0) 2025.03.03
[SMTP] SMTP 란?  (0) 2025.03.02