Добавлен механизм keepalive для WebSocket соединений
- Добавлен ping каждые 20 секунд в ChatConsumer - Добавлен ping каждые 20 секунд в ChatListConsumer - Добавлен ping каждые 20 секунд в NotificationConsumer - Клиенты игнорируют ping сообщения - Создан WEBSOCKET_KEEPALIVE_FIX.md с инструкциями для nginx - Исправляет проблему закрытия соединений через 27 секунд
This commit is contained in:
parent
67f118a1df
commit
480840407a
150
WEBSOCKET_KEEPALIVE_FIX.md
Normal file
150
WEBSOCKET_KEEPALIVE_FIX.md
Normal 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)
|
||||
@ -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 списка
|
||||
|
||||
@ -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):
|
||||
"""Получение сообщения от клиента"""
|
||||
|
||||
@ -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;
|
||||
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user