Heroku部署Django Channels时WebSocket连接404失败如何解决
问题现象
在Heroku平台部署集成Django Channels的Django项目后,从本地前端地址http://localhost:3000/向服务端发起WebSocket连接时失败,错误截图如下:
从运行日志可以看到,普通HTTP接口可正常返回200状态码,Heroku Redis插件运行状态正常,但所有WebSocket请求路径均返回404 Not Found。
相关代码与配置信息
前端WebSocket连接逻辑
useEffect(() => { Socket = new WebSocket(`${WEBSOCKET_URL}/room/${roomName}/${myUserName}/`); Socket.onmessage = ({ data }) => { let res = JSON.parse(data); if (!res.error && res["data-type"] === "begin-game") { const { gameId } = res; Socket.close(); navigate(`/game/${gameId}/`, { state: { gameId, data: res.data, myUserName }, }); } else if (!res.error) setUsersOnRoom({ ...res.data }); }; }, []);
其中WebSocket服务地址配置为:
export const WEBSOCKET_URL = "wss://ludo-thegameforlegends.herokuapp.com/ws";
后端Django配置
asgi.py配置
import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter,URLRouter from channels.auth import AuthMiddlewareStack from main.routing import websocket_urlpatterns as room_urlpatters from gameManager.routing import websocket_urlpatterns as gameManager_urlpatterns from channels.security.websocket import AllowedHostsOriginValidator os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'ludo.settings') websocket_urlpatterns = room_urlpatters + gameManager_urlpatterns application = ProtocolTypeRouter({ 'http':get_asgi_application(), 'websocket':AllowedHostsOriginValidator(AuthMiddlewareStack(URLRouter(websocket_urlpatterns))), })
Channel Layers配置
CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [(os.environ.get('REDIS_URL'), 6379)], }, }, }
WebSocket路由配置
from django.urls import path from .consumers import room_consumer websocket_urlpatterns = [ path('ws/room/<str:roomName>/<str:userName>/',room_consumer.as_asgi()), ]
WebSocket消费者代码
import json from .models import Room from gameManager.models import GameInstance from asgiref.sync import async_to_sync from .serializers import RoomSerializer from channels.generic.websocket import WebsocketConsumer from .utils import changeUserInDatabase,setDisconnectingUserToNoneInDatabase class room_consumer(WebsocketConsumer): def connect(self): self.accept() self.room_group_name = self.scope['url_route']['kwargs']['roomName'] self.user_name = self.scope['url_route']['kwargs']['userName'] async_to_sync(self.channel_layer.group_add)(self.room_group_name,self.channel_name) room = Room.objects.get(name=self.room_group_name) if not room.nameExists(self.user_name): room,userColor = changeUserInDatabase(self.room_group_name,self.user_name) self.color = userColor user_list = RoomSerializer(room).data async_to_sync(self.channel_layer.group_send)(self.room_group_name,{'type':'broadcast','error':None,'data-type':'user-joined-or-disconnected','data':user_list})
Heroku运行日志
2022-06-25T06:10:09.516891+00:00 app[web.1]: 10.1.56.26 - - [25/Jun/2022:06:10:09 +0000] "POST /api/create-or-verify/ HTTP/1.1" 200 54 "http://localhost:3000/" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36" 2022-06-25T06:10:09.515137+00:00 heroku[router]: at=info method=POST path="/api/create-or-verify/" host=ludo-thegameforlegends.herokuapp.com request_id=7d47025d-6589-407b-81a1-094184123a43 fwd="27.34.26.234" dyno=web.1 connect=0ms service=5ms status=200 bytes=410 protocol=https 2022-06-25T06:10:11.282950+00:00 app[web.1]: Not Found: /ws/room/asdf/ghhhjh/ 2022-06-25T06:10:11.283285+00:00 app[web.1]: 10.1.30.202 - - [25/Jun/2022:06:10:11 +0000] "GET /ws/room/asdf/ghhhjh/ HTTP/1.1" 404 2262 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36" 2022-06-25T06:10:11.284165+00:00 heroku[router]: at=info method=GET path="/ws/room/asdf/ghhhjh/" host=ludo-thegameforlegends.herokuapp.com request_id=a453dbc3-f64d-4598-8123-256ff300ab4d fwd="27.34.26.234" dyno=web.1 connect=0ms service=4ms status=404 bytes=2582 protocol=https 2022-06-25T06:10:12.129139+00:00 heroku[router]: at=info method=GET path="/ws/room/asdf/ghhhjh/" host=ludo-thegameforlegends.herokuapp.com request_id=7b668aa1-e9fa-45b6-ab76-601ea563d62c fwd="27.34.26.234" dyno=web.1 connect=0ms service=6ms status=404 bytes=2582 protocol=https 2022-06-25T06:10:12.128284+00:00 app[web.1]: Not Found: /ws/room/asdf/ghhhjh/ 2022-06-25T06:10:12.128987+00:00 app[web.1]: 10.1.16.221 - - [25/Jun/2022:06:10:12 +0000] "GET /ws/room/asdf/ghhhjh/ HTTP/1.1" 404 2262 "-" "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.0.0 Safari/537.36" 2022-06-25T06:07:06.000000+00:00 app[heroku-redis]: source=REDIS addon=redis-sinuous-30846 sample#active-connections=1 sample#load-avg-1m=0.25 sample#load-avg-5m=0.3 sample#load-avg-15m=0.36 sample#read-iops=0 sample#write-iops=0 sample#memory-total=15618680kB sample#memory-free=10552720kB sample#memory-cached=2820572kB sample#memory-redis=333744bytes sample#hit-rate=1 sample#evicted-keys=0
问题根因与修复步骤
日志里的404是Django HTTP路由层抛出的,说明WebSocket请求根本没走到Channels的路由处理逻辑,核心是Heroku启动应用的方式不对:默认用的gunicorn是WSGI服务器,不支持WebSocket协议,所有ws路径的请求被当成普通HTTP请求匹配Django的urlpatterns,找不到对应路由就返回404。按以下步骤修复即可:
修改启动配置,使用ASGI服务器启动应用
在项目根目录创建/修改Procfile文件,替换默认的gunicorn启动命令为Channels官方推荐的daphne ASGI服务器:web: daphne ludo.asgi:application --port $PORT --bind 0.0.0.0同时把
daphne添加到项目的requirements.txt依赖文件中,否则Heroku部署时会找不到daphne命令。修正Redis Channel Layer配置
Heroku提供的REDIS_URL环境变量已经包含认证信息、主机地址和端口,现有配置把URL和6379拆成元组传入会导致Redis连接失败,修改为:CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [os.environ.get('REDIS_URL')], }, }, }补充跨域访问配置
本地前端http://localhost:3000属于跨域请求,需要在settings.py中添加允许的访问来源,避免被Origin校验拦截:ALLOWED_HOSTS = [ "ludo-thegameforlegends.herokuapp.com", "localhost", "127.0.0.1" ] # 如果使用django-cors-headers,同步添加以下配置 CORS_ALLOWED_ORIGINS = [ "http://localhost:3000", "https://ludo-thegameforlegends.herokuapp.com" ]
所有配置修改完成后,提交代码重新推送到Heroku触发部署,WebSocket连接即可正常建立。
内容的提问来源于stack exchange,提问作者Sachin Bhattarai

