生产环境Django Channels Websocket连接失败求助(Nginx/Gunicorn/Daphne)
问题背景
DRF+React应用部署到Debian生产环境,Gunicorn的WSGI服务正常,但Django Channels的Websocket无法连接。本地python manage.py runserver时Websocket工作正常,所有服务(gunicorn socket、gunicorn service、daphne、nginx)均无报错,已尝试更换域名/IP/端口、切换ws/wss、添加Allowed Hosts,均无效。
相关代码文件
routing.py
from channels.routing import ProtocolTypeRouter, URLRouter from django.urls import path, re_path from apps.chat_app.consumers import ChatConsumer websocket_urlpatterns = [ path('ws/chat/<int:id>/<int:curr>/', ChatConsumer.as_asgi()), ] application = ProtocolTypeRouter({ 'websocket': URLRouter( websocket_urlpatterns ) , })
consumers.py
import json from channels.db import database_sync_to_async from channels.generic.websocket import AsyncWebsocketConsumer from django.contrib.auth import get_user_model from apps.chat_app.models import Message class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): current_user_id = self.scope['url_route']['kwargs']['curr'] other_user_id = self.scope['url_route']['kwargs']['id'] self.room_name = ( f'{current_user_id}_{other_user_id}' if int(current_user_id) > int(other_user_id) else f'{other_user_id}_{current_user_id}' ) self.room_group_name = f'chat_{self.room_name}' await self.channel_layer.group_add(self.room_group_name, self.channel_name) await self.accept() async def disconnect(self, close_code): await self.channel_layer.group_discard(self.room_group_name, self.channel_layer) await self.disconnect(close_code) async def receive(self, text_data=None, bytes_data=None): data = json.loads(text_data) message = data.get('message', '') sender_username = data['sender'].replace('"', '') sender = await self.get_user(username=sender_username) typing = data.get('typing', False) delete = data.get('delete', '') if typing: await self.channel_layer.group_send( self.room_group_name, { 'type': 'user_typing', 'sender': sender_username, 'msg': f'{sender.first_name.capitalize()} {sender.last_name.capitalize()} is typing...', } ) elif delete: await self.delete_message(msg_id=data['delete']) await self.channel_layer.group_send( self.room_group_name, { 'type': 'message_delete', 'msg_id': data['delete'], } ) else: await self.channel_layer.group_send( self.room_group_name, { 'type': 'user_typing', 'sender': sender_username, 'msg': '', } ) if message: msg = await self.save_message(sender=sender, message=message, thread_name=self.room_group_name) await self.channel_layer.group_send( self.room_group_name, { 'type': 'chat_message', 'msg_id': msg.id, 'message': message, 'sender': sender_username, 'timestamp': msg.timestamp.strftime('%d/%m/%Y %H:%M'), 'full_name': f'{sender.first_name.capitalize()} {sender.last_name.capitalize()}', }, ) async def message_delete(self, event): msg_id = event['msg_id'] await self.send( text_data=json.dumps( { 'delete': msg_id, } ) ) async def user_typing(self, event): username = event['sender'] msg = event['msg'] await self.send( text_data=json.dumps( { 'is_typing': True, 'sender': username, 'msg': msg, } ) ) async def chat_message(self, event): message = event['message'] username = event['sender'] full_name = event['full_name'] msg_id = event['msg_id'] timestamp = event['timestamp'] typing = event.get('typing', False) delete = event.get('delete', '') if typing: await self.send( text_data=json.dumps( { 'sender': username, 'typing': typing, } ) ) elif delete: await self.send( text_data=json.dumps( { 'delete': delete, } ) ) else: if message: await self.send( text_data=json.dumps( { 'msg_id': msg_id, 'message': message, 'timestamp': timestamp, 'sender': username, 'full_name': full_name, } ) ) @database_sync_to_async def get_user(self, username): return get_user_model().objects.filter(username=username).first() @database_sync_to_async def save_message(self, sender, message, thread_name): return Message.objects.create(sender=sender, message=message, thread_name=thread_name) @database_sync_to_async def delete_message(self, msg_id): Message.objects.filter(id=msg_id).delete()
asgi.py
import os from django.core.asgi import get_asgi_application from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter from channels.security.websocket import AllowedHostsOriginValidator os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'inp_proj.settings') django_asgi_app = get_asgi_application() import apps.chat_app.routing application = ProtocolTypeRouter( { 'http': django_asgi_app, 'websocket': AllowedHostsOriginValidator( AuthMiddlewareStack(URLRouter(apps.chat_app.routing.websocket_urlpatterns))), } )
daphne.service
[Unit] Description=WebSocket Daphne Service After=network.target [Service] Type=simple User=root WorkingDirectory=/www/projectdir ExecStart=/www/projectdir/venv/bin/python /www/projectdir/venv/bin/daphne -b 0.0.0.0 -p 8001 proj.asgi:application Restart=on-failure [Install] WantedBy=multi-user.target
gunicorn.service
[Unit] Description=gunicorn daemon Requires=gunicorn.socket After=network.target [Service] User=jan Group=www-data WorkingDirectory=/www/projectdir ExecStart=/www/projectdir/venv/bin/gunicorn \ --access-logfile - \ --workers 3 \ --bind unix:/run/gunicorn.sock \ proj.wsgi:application [Install] WantedBy=multi-user.target
gunicorn.socket
[Unit] Description=gunicorn socket [Socket] ListenStream=/run/gunicorn.sock [Install] WantedBy=sockets.target
Nginx配置
upstream websocket { server 127.0.0.1:8001; } server { server_name 127.0.0.1 mydomain; location = /favicon.ico { access_log off; log_not_found off; } location /static/ { root /www/projdir; } location / { include proxy_params; proxy_pass http://unix:/run/gunicorn.sock; } location /ws/ { proxy_pass http://websocket; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_redirect off; 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-Host $server_name; } }
客户端连接代码
const client = useMemo(() => { return new w3cwebsocket(`ws://mydomain:8001/ws/chat/${id}/${userId}/`); }, [id, userId]);
排查与修复步骤
1. 修正客户端连接地址
当前客户端直接连接8001端口,但Nginx已配置/ws/路径反向代理到Daphne,应让客户端通过Nginx的80/443端口连接,而非直接访问8001:
// 生产环境改为通过Nginx代理,不需要指定8001端口 const client = useMemo(() => { // 如果用HTTPS,改为wss://mydomain/ws/chat/${id}/${userId}/ return new w3cwebsocket(`ws://mydomain/ws/chat/${id}/${userId}/`); }, [id, userId]);
注意:如果服务器配置了HTTPS,必须用wss协议,否则浏览器会阻止混合内容请求。
2. 修复Consumer的disconnect方法死循环
consumers.py的disconnect方法调用了自身,会导致无限递归,同时group_discard参数错误:
async def disconnect(self, close_code): await self.channel_layer.group_discard(self.room_group_name, self.channel_name) await super().disconnect(close_code)
3. 检查Daphne的应用路径
daphne.service中ExecStart的proj.asgi:application要匹配项目ASGI模块名称(对应settings里的inp_proj.settings),需改为:
ExecStart=/www/projectdir/venv/bin/python /www/projectdir/venv/bin/daphne -b 0.0.0.0 -p 8001 inp_proj.asgi:application
4. 配置Channel Layer
确保Django settings中配置正确的Channel Layer(如Redis),否则Group相关操作会失效:
CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [("127.0.0.1", 6379)], }, }, }
5. 验证Nginx路径匹配
Nginx的location /ws/配置中,proxy_pass末尾不要加/,避免路径重写错误:
location /ws/ { proxy_pass http://websocket; # 其余配置不变 }
6. 查看Daphne日志
执行以下命令实时查看Daphne日志,确认连接请求是否到达:
journalctl -u daphne.service -f
7. 检查防火墙规则
确保服务器防火墙允许80/443端口(通过Nginx代理):
ufw allow 80/tcp ufw allow 443/tcp
内容的提问来源于stack exchange,提问作者4bs3nt

