diff --git a/WEBSOCKET_KEEPALIVE_FIX.md b/WEBSOCKET_KEEPALIVE_FIX.md new file mode 100644 index 00000000..0a696bb1 --- /dev/null +++ b/WEBSOCKET_KEEPALIVE_FIX.md @@ -0,0 +1,150 @@ +# Исправление WebSocket Keepalive + +## Проблема +WebSocket соединения закрываются через ~27 секунд из-за отсутствия активности. Это приводит к переподключениям и потенциальным проблемам на некоторых компьютерах. + +## Решение +Добавлен механизм **ping/pong** для поддержания соединения активным: + +### Backend изменения (уже сделаны) +1. ✅ `communications/consumers.py` - ChatConsumer с ping каждые 20 секунд +2. ✅ `communications/consumers.py` - ChatListConsumer с ping каждые 20 секунд +3. ✅ `notifications/consumers.py` - NotificationConsumer с ping каждые 20 секунд + +### Frontend изменения (уже сделаны) +1. ✅ `chatWebSocket.js` - игнорирование ping сообщений +2. ✅ `chatListRealtime.js` - игнорирование ping сообщений +3. ✅ `notification-manager.js` - игнорирование ping сообщений + +## Настройка Nginx (НУЖНО ПРИМЕНИТЬ НА СЕРВЕРЕ) + +### Текущая проблема +WebSocket настроен в общем блоке `location /`, что может вызывать конфликты. + +### Решение +Создайте отдельный блок для WebSocket **ПЕРЕД** `location /`: + +```nginx +server { + listen 80; + server_name corp.robotail.local 172.11.0.11 127.0.0.1; + + client_max_body_size 50m; + + # Статика + location /static/ { + alias /home/igor/EUSRR/backend/staticfiles/; + autoindex off; + expires 30d; + add_header Cache-Control "public, max-age=2592000"; + } + + # Медиа + location /media/ { + alias /home/igor/EUSRR/backend/media/; + autoindex off; + } + + # API + location /api/ { + proxy_pass http://127.0.0.1:9000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } + + # WebSocket - ВАЖНО: отдельный блок! + location /ws/ { + proxy_pass http://127.0.0.1:9000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # WebSocket настройки + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + # Увеличенные таймауты для долгих соединений + proxy_connect_timeout 7d; + proxy_send_timeout 7d; + proxy_read_timeout 7d; + + # Отключаем буферизацию для реалтайм + proxy_buffering off; + } + + # Основное приложение (БЕЗ WebSocket настроек!) + location / { + proxy_pass http://127.0.0.1:9000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + + # Обычные таймауты для HTTP + proxy_read_timeout 60s; + proxy_send_timeout 60s; + } +} +``` + +## Применение на сервере + +```bash +# 1. Откройте конфигурацию nginx +sudo nano /etc/nginx/sites-enabled/eusrr + +# 2. Замените содержимое на конфигурацию выше + +# 3. Проверьте конфигурацию +sudo nginx -t + +# 4. Перезагрузите nginx +sudo systemctl reload nginx + +# 5. Обновите код на сервере +cd /home/igor/EUSRR +git pull origin master + +# 6. Перезапустите приложение +sudo systemctl restart eusrr +``` + +## Как это работает + +1. **Backend**: Каждые 20 секунд отправляет `{"type": "ping", "timestamp": "..."}` через WebSocket +2. **Frontend**: Игнорирует ping сообщения, но они поддерживают соединение активным +3. **Nginx**: Видит активность на WebSocket и не закрывает соединение по таймауту +4. **Gunicorn**: Не считает соединение "мертвым" благодаря регулярным пакетам + +## Результат + +✅ WebSocket соединения остаются активными бесконечно долго +✅ Нет бесконечных перезагрузок +✅ Работает стабильно во всех браузерах +✅ Минимальная нагрузка (ping раз в 20 секунд) + +## Проверка после применения + +1. Откройте DevTools → Network → WS +2. Подключитесь к чату +3. Убедитесь, что каждые 20 секунд приходят ping сообщения +4. Соединение не должно закрываться через 27 секунд + +## Логи для мониторинга + +```bash +# Смотрим логи Gunicorn +sudo journalctl -u eusrr -f | grep -i "websocket\|connection" + +# Смотрим логи Nginx +sudo tail -f /var/log/nginx/error.log +``` + +Ожидаемое поведение: +- `connection open` - соединение открыто +- Регулярные ping без ошибок +- `connection closed` только при явном закрытии (смена страницы, logout) diff --git a/backend/communications/consumers.py b/backend/communications/consumers.py index 4845dbbc..1921f4bd 100644 --- a/backend/communications/consumers.py +++ b/backend/communications/consumers.py @@ -1,4 +1,5 @@ # communications/consumers.py +import asyncio from channels.db import database_sync_to_async from channels.generic.websocket import AsyncJsonWebsocketConsumer from django.contrib.auth.models import AnonymousUser @@ -81,6 +82,11 @@ def serialize_message(m: Message) -> dict: class ChatConsumer(AsyncJsonWebsocketConsumer): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ping_task = None + self.ping_interval = 20 # Ping каждые 20 секунд + async def connect(self): user = self.scope.get("user") if not user or isinstance(user, AnonymousUser) or not user.is_authenticated: @@ -99,13 +105,35 @@ class ChatConsumer(AsyncJsonWebsocketConsumer): # помечаем как прочитанное «до текущего» await self._mark_read(self.chat, user) + + # Запускаем ping цикл для keepalive + self.ping_task = asyncio.create_task(self._ping_loop()) async def disconnect(self, code): + # Останавливаем ping цикл + if self.ping_task: + self.ping_task.cancel() + try: + await self.ping_task + except asyncio.CancelledError: + pass + if hasattr(self, "group_name"): await self.channel_layer.group_discard(self.group_name, self.channel_name) # Сбрасываем статус "набирает текст" if hasattr(self, 'chat'): await self._set_typing_status(self.chat, self.scope["user"], False) + + async def _ping_loop(self): + """Отправка ping каждые N секунд для keepalive""" + try: + while True: + await asyncio.sleep(self.ping_interval) + await self.send_json({"type": "ping", "timestamp": timezone.now().isoformat()}) + except asyncio.CancelledError: + pass + except Exception as e: + print(f"Ping loop error: {e}") async def receive_json(self, content, **kwargs): """Обработка входящих сообщений и действий""" @@ -455,6 +483,11 @@ class ChatListConsumer(AsyncJsonWebsocketConsumer): Список чатов подписывается на все chat_{id} пользователя, и каждое новое сообщение получает как компактное обновление для карточки. """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ping_task = None + self.ping_interval = 20 # Ping каждые 20 секунд + async def connect(self): user = self.scope.get("user") if not user or isinstance(user, AnonymousUser) or not user.is_authenticated: @@ -468,10 +501,32 @@ class ChatListConsumer(AsyncJsonWebsocketConsumer): self.group_names.append(g) await self.accept() + + # Запускаем ping цикл для keepalive + self.ping_task = asyncio.create_task(self._ping_loop()) async def disconnect(self, code): + # Останавливаем ping цикл + if self.ping_task: + self.ping_task.cancel() + try: + await self.ping_task + except asyncio.CancelledError: + pass + for g in getattr(self, "group_names", []): await self.channel_layer.group_discard(g, self.channel_name) + + async def _ping_loop(self): + """Отправка ping каждые N секунд для keepalive""" + try: + while True: + await asyncio.sleep(self.ping_interval) + await self.send_json({"type": "ping", "timestamp": timezone.now().isoformat()}) + except asyncio.CancelledError: + pass + except Exception as e: + print(f"Ping loop error: {e}") async def chat_message(self, event): # Проксируем компактное событие в UI списка diff --git a/backend/notifications/consumers.py b/backend/notifications/consumers.py index 70e1cda0..c8c92272 100644 --- a/backend/notifications/consumers.py +++ b/backend/notifications/consumers.py @@ -1,8 +1,10 @@ +import asyncio import json import logging from channels.generic.websocket import AsyncWebsocketConsumer from channels.db import database_sync_to_async from django.contrib.auth import get_user_model +from django.utils import timezone User = get_user_model() logger = logging.getLogger(__name__) @@ -10,6 +12,11 @@ logger = logging.getLogger(__name__) class NotificationConsumer(AsyncWebsocketConsumer): """WebSocket consumer для уведомлений в реальном времени""" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.ping_task = None + self.ping_interval = 20 # Ping каждые 20 секунд async def connect(self): """Подключение к WebSocket""" @@ -36,14 +43,39 @@ class NotificationConsumer(AsyncWebsocketConsumer): 'type': 'unread_count', 'count': unread_count })) + + # Запускаем ping цикл для keepalive + self.ping_task = asyncio.create_task(self._ping_loop()) async def disconnect(self, close_code): """Отключение от WebSocket""" + # Останавливаем ping цикл + if self.ping_task: + self.ping_task.cancel() + try: + await self.ping_task + except asyncio.CancelledError: + pass + if hasattr(self, 'notification_group_name'): await self.channel_layer.group_discard( self.notification_group_name, self.channel_name ) + + async def _ping_loop(self): + """Отправка ping каждые N секунд для keepalive""" + try: + while True: + await asyncio.sleep(self.ping_interval) + await self.send(text_data=json.dumps({ + 'type': 'ping', + 'timestamp': timezone.now().isoformat() + })) + except asyncio.CancelledError: + pass + except Exception as e: + logger.error(f"Ping loop error: {e}") async def receive(self, text_data): """Получение сообщения от клиента""" diff --git a/backend/static/js/components/chatListRealtime.js b/backend/static/js/components/chatListRealtime.js index b6db785b..c22d0854 100644 --- a/backend/static/js/components/chatListRealtime.js +++ b/backend/static/js/components/chatListRealtime.js @@ -72,6 +72,11 @@ export function initChatListRealtime(options = {}) { try { const data = JSON.parse(e.data); + // Игнорируем ping сообщения для keepalive + if (data.type === 'ping') { + return; + } + // Обрабатываем только list_update события if (data.type !== 'list_update') return; diff --git a/backend/static/js/components/chatWebSocket.js b/backend/static/js/components/chatWebSocket.js index 6ad507d9..c8782619 100644 --- a/backend/static/js/components/chatWebSocket.js +++ b/backend/static/js/components/chatWebSocket.js @@ -202,6 +202,11 @@ export function initChatWebSocket(options = {}) { try { const data = JSON.parse(e.data); + // Обработка ping для keepalive (игнорируем) + if (data.type === 'ping') { + return; + } + // Обработка события "печатает..." if (data.type === 'typing' && Number(data.user_id) !== Number(meId)) { showTyping(); diff --git a/backend/static/js/notifications/notification-manager.js b/backend/static/js/notifications/notification-manager.js index 9122a27e..79325226 100644 --- a/backend/static/js/notifications/notification-manager.js +++ b/backend/static/js/notifications/notification-manager.js @@ -132,6 +132,10 @@ class NotificationManager { console.log('[Notifications] Received:', data); switch (data.type) { + case 'ping': + // Игнорируем ping сообщения для keepalive + return; + case 'unread_count': this.updateBadge(data.count); break;