Добавлен механизм keepalive для WebSocket соединений

- Добавлен ping каждые 20 секунд в ChatConsumer
- Добавлен ping каждые 20 секунд в ChatListConsumer
- Добавлен ping каждые 20 секунд в NotificationConsumer
- Клиенты игнорируют ping сообщения
- Создан WEBSOCKET_KEEPALIVE_FIX.md с инструкциями для nginx
- Исправляет проблему закрытия соединений через 27 секунд
This commit is contained in:
admin i 2025-11-28 13:57:59 +03:00
parent 1127af354e
commit a2ceba626c
6 changed files with 251 additions and 0 deletions

150
WEBSOCKET_KEEPALIVE_FIX.md Normal file
View File

@ -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)

View File

@ -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 списка

View File

@ -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):
"""Получение сообщения от клиента"""

View File

@ -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;

View File

@ -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();

View File

@ -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;