背景
我有一个用于基础数据库表的Handsontable实现。即“外观和感觉就像电子表格的JavaScript数据网格”。
在其他浏览器选项卡/窗口中打开工作表时,对一张纸上的单元格所做的更改应在其他用户的同一张纸上反映出来。
这要求使用Web套接字进行“服务器端推送”。即服务器需要推送通知以打开“客户端”。
另一种方法是让客户端浏览器进行Ajax轮询以进行更改。但这比较浪费。仅在保存有效更改后更新工作表!
团队内部使用此应用程序。使用量不超过十个并发用户。这意味着:
一个处理Web套接字请求的过程就足够了。
没有进行真正的性能测试。
选择daphne
在我的情况下,无需异步处理websocket交互。想深入了解的可以在Django频道中了解有关使用WebSocket进行同步与异步的更多信息。
该配置将保持不变,直到出现问题为止。因为“过早的优化是万恶之源”。
蓝图:之前和之后
注意:下面的http请求协议既指http也指https。同样适用于ws和wss。假设对于本地开发,该协议是不安全的,但在生产中是安全的。
在引入websocket之前,Web浏览器向Nginx发出了http请求。此时,Nginx使用gunicorn来处理请求,并点击Django1。
在将Websocket添加到混合中之后,Nginx仍然可以处理http请求。但是现在,通过与daphne交谈,它可以满足ws请求。在这种情况下,您可以将daphne替换为任何其他Websocket终止服务器:
https://www.untangled.dev/assets/img/posts/2020-08-02-django-websockets/blueprint.png
因此,上面构建块中的“新”项目是daphne:
Daphne是用于ASGI和ASGI-HTTP的HTTP,HTTP2和WebSocket协议服务器,旨在为Django通道提供支持。
它支持协议的自动协商;无需使用URL前缀即可确定WebSocket端点与HTTP端点。
daphne成分可以用uvicorn或starlette替代。
另一个需要注意的地方是ws://连接是一个“开放”连接。 “数据”沿着同一套接字在两个方向上传播。相对于http://。
代码变更
daphne是Django Channels工作的一部分:
Channels使用熟悉的Django设计模式和灵活的基础框架来扩展Django,为代码提供WebSocket,长轮询HTTP,任务分载和其他异步支持,该框架不仅使您可以自定义行为,还可以为自己的协议和需求编写支持。
为了完整起见,这些代码更改适用于使用以下软件包版本的安装:
1 2 3 4 |
channels==2.4.0 channels-redis==3.0.1 Django==3.0.8 redis==3.5.3 |
在Python 3.7.5 virtualenv中。
需要进行的更改:
settings.py更改。 这些:
将channels添加到INSTALLED_APPS,并
配置通道以将Websocket请求路由到主要通道入口点
路由代码更改:
主项目级路由入口点
应用程序级别入口点,在此示例中,仅使用一个示例myapp作为应用程序
托管我们应用程序需要实现的所有事件处理和消息发送逻辑的使用者。
1.设置模块更改
在我的项目的INSTALLED_APPS列表中将channels添加为第一个应用。 为什么要先?
请警惕需要重载或替换runserver命令的任何其他第三方应用程序。 channels提供了单独的runserver命令,并且可能与之冲突。 此类冲突的一个示例是whitenoise中的whitenoise.runserver_nostatic。 为了解决此类问题,请尝试将频道移至INSTALLED_APPS的顶部或完全删除有问题的应用程序。
然后添加此新设置以供渠道应用使用:
1 2 3 4 5 6 7 8 9 10 |
# CHANNELS ASGI_APPLICATION = 'proj.routing.application' CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { 'hosts': [('127.0.0.1', 6379)], }, }, } |
请注意,我已经安装了Redis进行缓存以及使用Huey应用程序的现有任务队列。
2.路由变更
我通常将其称为“默认”应用程序项目。 很明显,该应用程序是项目范围内项目的容器。 与这个新的路由模块一样。 它包含ProtocolTypeRouter,用作ASGI应用程序的主要入口点。 proj / routing.py内容:
1 2 3 4 5 6 7 8 9 10 11 12 |
from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter import myapp.routing application = ProtocolTypeRouter({ # (http->django views is added by default) 'websocket': AuthMiddlewareStack( URLRouter( myapp.routing.websocket_urlpatterns ) ), }) |
myapp是用于此示例的测试应用程序。 上面的顶级路由器包含对myapp.routing模块的引用。 此URLRouter通过其HTTP路径路由http或websocket类型的连接。 myapp / routing.py包含以下内容:
1 2 3 4 5 6 7 |
from django.urls import re_path from myapp import consumers websocket_urlpatterns = [ re_path(r'ws/sheet/(?P<sheet_name>\w+)/$', consumers.SheetConsumer), ] |
请注意,渠道如何使我们能够以我们熟悉的标准urls.py configuration2所使用的格式来构造Web套接字URL。
3.消费者
需要添加的最后一个模块是使用者。 在channels中,消费者:
将代码构造为一系列事件发生时要调用的函数,而不是使编写事件循环。
允许编写同步或异步代码,并为处理切换和线程。
myapp / consumers.py实现了SheetConsumer类,该类扩展了WebsocketConsumer:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 |
import json from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer class SheetConsumer(WebsocketConsumer): def connect(self): self.sheet_name = self.scope['url_route']['kwargs']['sheet_name'] self.sheet_group_name = 'sheet_%s' % self.sheet_name # Join sheet group async_to_sync(self.channel_layer.group_add)( self.sheet_group_name, self.channel_name ) self.accept() def disconnect(self, close_code): # Leave sheet group async_to_sync(self.channel_layer.group_discard)( self.sheet_group_name, self.channel_name ) # Receive message from WebSocket def receive(self, text_data): text_data_json = json.loads(text_data) # Send sheet_name to sheet group async_to_sync(self.channel_layer.group_send)( self.sheet_group_name, { 'type': 'refresh_sheet', 'sheet_name': text_data_json['sheet_name'], 'object_id': text_data_json['object_id'], 'column_index': text_data_json['column_index'], 'new_value': text_data_json['new_value'], 'broadcaster_id': text_data_json['broadcaster_id'], } ) # Receive message from sheet group def refresh_sheet(self, event): # Send sheet_name to WebSocket self.send(text_data=json.dumps({ 'sheet_name': event['sheet_name'], 'object_id': event['object_id'], 'column_index': event['column_index'], 'new_value': event['new_value'], 'broadcaster_id': event['broadcaster_id'], })) |
以上内容基于“编写的第一个消费者教程”部分。 数据不是聊天消息,而是有关工作表单元格更新的数据。 在其他浏览器选项卡/窗口中打开需要用于同一工作表的更新。
我正在通过broadcaster_id传递经过身份验证的用户的用户ID。 为了能够知道哪个用户“触发”了“广播”的websocket消息。
在本地尝试一下
这是频道的另一个重要功能。 甚至不需要平常的manage.py runserver工作流也需要更改。 只需在启动时在默认runserver输出中记录新项即可:
1 2 3 4 5 6 7 8 9 |
$ ./manage.py runserver Watching for file changes with StatReloader Performing system checks... System check identified no issues (0 silenced). August 01, 2020 - 16:07:41 Django version 3.0.8, using settings 'proj.settings.local' Starting ASGI/Channels version 2.4.0 development server at http://127.0.0.1:8000/ << THIS! Quit the server with CONTROL-C. |
套接字握手也显示在输出中:
1 2 |
WebSocket HANDSHAKING /ws/sheet/sheet1/ [127.0.0.1:65181] WebSocket CONNECT /ws/sheet/sheet1/ [127.0.0.1:65181] |
部署说明
没那么快?
我遵循了此处的渠道文档以及Django自己的有关在此处部署ASGI应用程序的文档。但是我想解释两个调整。
daphne命令调整
我遇到了监听失败:[Errno 88]此处描述的针对非套接字异常的套接字操作。
通过删除-fd 0开关的建议可以解决此问题。默认情况下,在频道文档中建议使用此开关。
在当前用例中,我不需要使用此开关。因为我不需要将多个Daphne实例绑定到我的生产实例的同一端口。如果我这样做了,我将需要更改我的结构(请参阅下一节),以直接从主管那里调用daphne。而不是通过bash脚本。
asgi.py调整
我已经实现了proj / asgi.py,如此处的渠道文档所述。这在本地工作正常。但这导致了在生产中执行Web套接字请求时在此描述的异常。我按照此stackoverflow答案中所述更改了proj / asgi.py,使其具有以下内容:
1 2 3 4 5 6 7 8 9 |
import os import django from channels.routing import get_default_application os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'proj.settings') django.setup() application = get_default_application() |
此更改将django.core.asgi.get_asgi_application的用法替换为channels.routing.get_default_application。根据此处的渠道文档,支持上面的stackoverflow答案。
我不知道这是否可以正确解决问题。我应该做些其他的事情吗?频道和Django文档之间似乎有点不兼容。渠道文档建议从头开始创建asgi.py。而Django 3.0.8自动创建了asgi.py。
如果您对此有更好的解决方法,请告诉我(以下评论)。
Redis版本
回想一下,我的配置正在使用channels_redis作为后备存储。
由于我的生产应用程序在Ubuntu 18.04 LTS上运行,因此默认的apt-get redis版本为4.0.1。
这导致出现奇怪的BZPOPMIN-“ ERR未知命令’BZPOPMIN’”错误。这是因为需要redis版本5或更高版本。
因此,请升级redis以进行配置。就我而言,我遵循了快速入门文档,特别是“更正确地安装Redis”部分。
转到生产配置文件!
部署-结果配置
我的配置的组件:
可执行的bash脚本运行daphne。我使用它可以直接在Django项目的virtualenv中运行和测试daphne。
管理员conf文件,用于由管理员管理此bash脚本过程。
Nginx,当然。
Bash脚本
start_daphne.bash内容。记住要chmod + x bash脚本。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#!/bin/bash NAME="myproject-daphne" # Name of the application DJANGODIR=/home/ubuntu/webapp/myproject/proj # Django project directory DJANGOENVDIR=/home/ubuntu/webapp/myprojectenv # Django project env echo "Starting $NAME as `whoami`" # Activate the virtual environment cd $DJANGODIR source /home/ubuntu/webapp/myprojectenv/bin/activate source /home/ubuntu/webapp/myproject/proj/.env export PYTHONPATH=$DJANGODIR:$PYTHONPATH # Start daphne exec ${DJANGOENVDIR}/bin/daphne -u /home/ubuntu/webapp/myprojectenv/run/daphne.sock --access-log - --proxy-headers proj.asgi:application |
Supervisor
文件位于:/etc/supervisor/conf.d/daphne.conf。 记住要创建日志文件目录。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
; ================================ ; daphne supervisor ; ================================ [program:daphne] command = /home/ubuntu/webapp/start_daphne.bash ; Command to start app user = ubuntu ; User to run as numprocs=1 autostart=true autorestart=true redirect_stderr=true stdout_logfile = /home/ubuntu/webapp/logs/daphne/access.log ; Where to write access log messages stderr_logfile = /home/ubuntu/webapp/logs/daphne/error.log ; Where to write error log messages stdout_logfile_maxbytes=50MB stderr_logfile_maxbytes=50MB stdout_logfile_backups=10 stderr_logfile_backups=10 environment=LANG=en_US.UTF-8,LC_ALL=en_US.UTF-8 ; Set UTF-8 as default encoding |
Nginx的
我使用的Nginx配置参考:通道部署文档以及关于stackoverflow的答案。 请点击这些链接以了解我的所作所为。
相关的Nginx配置内容:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
upstream ws_server { server unix:/home/ubuntu/webapp/myprojectenv/run/daphne.sock fail_timeout=0; } upstream gunicorn_server { server unix:/home/ubuntu/webapp/myprojectenv/run/gunicorn.sock fail_timeout=0; } ... server { ... location /ws/ { proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; proxy_redirect off; proxy_pass http://ws_server; } location / { ... if (!-f $request_filename) { proxy_pass http://gunicorn_server; break; } } } |
请注意新添加的与ws_server相关的部分。
注释标记/ JavaScript代码
这不是这样的配置。 但是,您可以看到整个教程都没有解决ws和wss的用法。 原因之一是在本项目中,Nginx不处理SSL证书部分。 由于该项目使用Cloudflare SSL,因此即使在“ Nginx之前”,Cloudflare也会对其进行处理。
我唯一的ws vs wss逻辑是在客户端级别完成的。 这允许相同的代码在本地和生产环境中使用正确的协议。 该代码根据当前使用的http协议来建立连接:
1 2 3 4 5 6 7 8 9 10 |
... if (window.location.protocol == 'https:') { wsProtocol = 'wss://' } else {wsProtocol = 'ws://'} sheetSocket = new WebSocket( wsProtocol + window.location.host + '/ws/sheet/' + sheetName + '/' ); |
结论
请让我知道(在下面的评论中)我做的任何事情是错误的还是可以改进的。
这是我第一次使用Websockets和Django的经验。 这是令人愉快的。 上面描述的少数几个“冲突文档”问题虽然被阻止,但还是可以预期的。
原文:https://www.untangled.dev/2020/08/02/django-websockets-minimal-setup/