Async і Chat¶
WebSocket-чат є основним async-компонентом проєкту.
Файли¶
| Файл | Роль |
|---|---|
notes_project/asgi.py |
ProtocolTypeRouter — розподіл HTTP vs WebSocket |
notes_project/routing.py |
WebSocket URL patterns |
notes_app/consumers.py |
GroupChatConsumer(AsyncWebsocketConsumer) |
notes_app/static/notes_app/js/group_chat.js |
JS-клієнт (WebSocket API) |
notes_app/templates/notes_app/group_chat.html |
Шаблон сторінки чату |
ASGI routing¶
Browser WebSocket: ws://host/ws/groups/42/chat/
│
▼
Nginx (proxy_http_version 1.1 + Upgrade/Connection headers)
│
▼
Uvicorn (ASGI server)
│
▼
ProtocolTypeRouter
"websocket" → AuthMiddlewareStack
↓
URLRouter
↓
/ws/groups/<pk>/chat/ → GroupChatConsumer
AuthMiddlewareStack читає session cookie з WS handshake і наповнює scope["user"].
GroupChatConsumer — lifecycle¶
connect()¶
- Читає
group_pkзscope["url_route"]["kwargs"]. - Перевіряє
scope["user"].is_authenticated→close(4003)якщо ні. - Перевіряє членство в групі через
database_sync_to_async—close(4004)якщо не member. - Завантажує останні 50 повідомлень і відправляє клієнту.
- Додає канал до room group:
channel_layer.group_add(room_group_name, self.channel_name). accept()— рукостискання WebSocket.
receive()¶
- Парсить JSON:
{"message": "..."}. - Перевіряє
content— якщо порожній, ігнорує. - Зберігає
ChatMessageчерезdatabase_sync_to_async. - Публікує подію у channel group:
channel_layer.group_send(room_group_name, {...}). - Кожен consumer у групі отримує подію через
chat_message()і відправляє JSON клієнту.
disconnect()¶
- Видаляє канал з room group:
channel_layer.group_discard(...).
database_sync_to_async¶
Django ORM є синхронним. У async consumer всі ORM-операції мають виконуватись у worker thread через database_sync_to_async:
from channels.db import database_sync_to_async
@database_sync_to_async
def _get_recent_messages(self):
messages = ChatMessage.objects.filter(group=self.group).order_by('timestamp')[:50]
return list(messages.values('author__username', 'content', 'timestamp'))
list(queryset.values(...)) — обов'язкове. Lazy QuerySet, що повертається в async context, викличе SynchronousOnlyOperation.
Channel Layer¶
Без REDIS_URL (локально без Docker)¶
Придатно лише для одного процесу. Повідомлення не передаються між різними uvicorn workers.
З REDIS_URL (Docker / production)¶
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels_redis.core.RedisChannelLayer",
"CONFIG": {
"hosts": [_REDIS_URL],
"socket_timeout": None, # критично — без цього TimeoutError при idle
},
}
}
socket_timeout=None — без нього RedisChannelLayer закриває з'єднання після idle timeout → TimeoutError при наступному повідомленні.
Тестування consumer (TransactionTestCase)¶
from django.test import TransactionTestCase
class GroupChatConsumerTest(TransactionTestCase):
def setUp(self): # SYNC setUp, не asyncSetUp
...
TransactionTestCase (не TestCase) використовується тому, що TestCase загортає кожен тест у транзакцію, яку database_sync_to_async worker thread не бачить. TransactionTestCase не загортає → дані видимі з усіх потоків.
XSS захист¶
JavaScript-клієнт екранує HTML перед вставкою у DOM:
function escapeHtml(text) {
const div = document.createElement('div');
div.appendChild(document.createTextNode(text));
return div.innerHTML;
}
Вміст повідомлень вставляється через escapeHtml(), а не через innerHTML напряму.