


서버와 클라이언트 간 실시간 양방향 통신 방법
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에서 HTTP통신을 업그레이드하여 웹 소켓, 채팅 프로토콜, IoT 프로토콜 등을 처리 할 수 있는 프로젝트
pip install channels
settings.py
INSTALLED_APPS = [
...
"canvas",
]
asgi.py
application = ProtocolTypeRouter(
{
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(websocket_urlpatterns)
),
}
)
canvas/urls.py
urlpatterns = [
re_path(r'ws/canvases/(?P<canvas_id>\w+)/$', CanvasConsumer.as_asgi(), name='canvas-edit'),
]
Settings.py
INSTALLED_APPS = [
"Daphne",
...
"canvas",
]
ASGI_APPLICATION = 'backend.asgi.application'
이제 프론트엔드에서 만든 페이지에서 다음과 같이 소켓 연결을 작성한다.
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로 전달된 데이터들을 웹 소켓으로 전송한다.
즉, 사용자가 메세지를 보내면 해당 메세지를 그룹에 있는 다른 클라이언트들에게 전송하고, 그룹으로부터 전달된 메세지를 웹 소켓을 통해 해당 클라이언트에게 전송하는 역할을 한다.