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 |