You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

生产环境Django Channels Websocket连接失败求助(Nginx/Gunicorn/Daphne)

Django Channels Websocket 生产环境连接失败排查

问题背景

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 07:55:01