在Django部署的时候,通常使用的都是WSGI(Web Server Gateway Interface)既通用服务网关接口,该协议仅用来处理 Http 请求,更多关于WSGI的说明请参见廖雪峰博客。
当网址需要加入 WebSocket 功能时,WSGI 将不再满足我们的需求,此时我们需要使用ASGI既异步服务网关接口,该协议能够用来处理多种通用协议类型,包括HTTP、HTTP2 和 WebSocket,更多关于 ASGI 的说明请参见此处。
ASGI 由 Django 团队提出,为了解决在一个网络框架里(如 Django)同时处理 HTTP、HTTP2、WebSocket 协议。为此,Django 团队开发了 Django Channels 插件,为 Django 带来了 ASGI 能力。
在 ASGI 中,将一个网络请求划分成三个处理层面,最前面的一层,interface server(协议处理服务器),负责对请求协议进行解析,并将不同的协议分发到不同的 Channel(频道);频道属于第二层,通常可以是一个队列系统。频道绑定了第三层的 Consumer(消费者)。
玩转 ASGI:从零到一实现一个实时博客
本文基于Django==2.1,channels==2.1.3,channels-redis==2.3.0。
示例项目RestaurantOrder旨在实现一个基于WebSocket的聊天室,在Channels 2.1.3文档中Tutorial的基础上稍加修改用于微信点餐过程中的多人协作点餐。
在 settings.py
加入和 channels 相关的基础设置:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
...
]
ASGI_APPLICATION = "RestaurantOrder.routing.application"
# WebSocket
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
在 wsgi.py
同级目录新增文件 asgi.py
:
"""
ASGI entrypoint. Configures Django and then runs the application
defined in the ASGI_APPLICATION setting.
"""
import os
import django
from channels.routing import get_default_application
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "RestaurantOrder.settings")
django.setup()
application = get_default_application()
在 wsgi.py
同级目录新增文件 routing.py
,其作用类型与 urls.py
,用于分发webscoket
请求:
from django.urls import path
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from table.consumers import TableConsumer
application = ProtocolTypeRouter({
# Empty for now (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter([
path('ws/table//', TableConsumer),
])
),
})
新增 app 名为 table
,在 table
目录下新增 consumers.py
:
from channels.generic.websocket import AsyncJsonWebsocketConsumer
from table.models import Table
class TableConsumer(AsyncJsonWebsocketConsumer):
table = None
async def connect(self):
self.table = 'table_{}'.format(self.scope['url_route']['kwargs']['table_id'])
# Join room group
await self.channel_layer.group_add(self.table, self.channel_name)
await self.accept()
async def disconnect(self, close_code):
# Leave room group
await self.channel_layer.group_discard(self.table, self.channel_name)
# Receive message from WebSocket
async def receive_json(self, content, **kwargs):
# Send message to room group
await self.channel_layer.group_send(self.table, {'type': 'message', 'message': content})
# Receive message from room group
async def message(self, event):
message = event['message']
# Send message to WebSocket
await self.send_json(message)
TableConsumer
类中的函数依次用于处理连接、断开连接、接收消息和处理对应类型的消息,其中channel_layer.group_send(self.table, {'type': 'message', 'message': content})
方法,self.table
参数为当前组的组id, {'type': 'message', 'message': content}
部分分为两部分,type
用于指定该消息的类型,根据消息类型调用不同的函数去处理消息,而 message
内为消息主体。
在 table
目录下的 views.py
中新增函数:
def table(request, table_id):
return render(request, 'table/table.html', {
'room_name_json': mark_safe(json.dumps(table_id))
})
table
函数对应的 urls.py
不再赘述。
在 table
的 templates\table
目录下新增 table.html
:
Chat Room
最终效果:
在官方文档中推荐Djaogo-Channels
的http
部分和websocket
部分均使用daphne
进行部署,该方法参见DjangoChannels Docs。
本文使用的方法为使用Nginx
代理,将http
部分请求发送给uwsgi
进行处理,将websocket
部分请求发送给daphne
进行处理。uwsgi
和daphhe
均使用supervisord
进行控制。
需要注意的是,由于Nginx
无法识别http
请求和websocket
请求,需要通过路由来区分是哪种协议。我使用的方法是规定所有的websocket的路由均以/ws开头(如: ws://www.example/ws/table/table_id/
),这样就可以让Nginx
将所有以/ws
开头的请求全部转发给daphne
进行处理。
在Nginx
和daphne
进行通信时,有http socket
和file socket
两种通信方式,推荐使用后一种file socket
的方式,在这里列出两种通信方式的部署代码。
http socket方式
nginx.conf:
upstream restaurant_order {
server unix:///django/RestaurantOrder/restaurant_order.sock;
}
server {
listen 8000;
server_name 114.116.25.246; # substitute your machine's IP address or FQDN
charset utf-8;
client_max_body_size 75M;
location /media {
alias /django/RestaurantOrder/media;
}
location /static {
alias /django/RestaurantOrder/static;
}
access_log /django/RestaurantOrder/log/access.log;
error_log /django/RestaurantOrder/log/error.log;
location / {
uwsgi_pass restaurant_order;
include /django/RestaurantOrder/uwsgi_params;
}
location /ws {
proxy_pass http://127.0.0.1:8001;
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;
proxy_read_timeout 36000s;
proxy_send_timeout 36000s;
}
}
supervisord.conf:
[program:restaurant_order_service]
command=uwsgi --ini /django/RestaurantOrder/restaurant_order_uwsgi.ini
directory=/django/RestaurantOrder
stdout_logfile=/django/RestaurantOrder/log/uwsgi_out.log
stderr_logfile=/django/RestaurantOrder/log/uwsgi_err.log
autostart=true
autorestart=true
user=root
startsecs=10
[program:restaurant_order_websocket]
command=/django/RestaurantOrder/environment/bin/daphne -b 0.0.0.0 -p 8001 RestaurantOrder.asgi:application
directory=/django/RestaurantOrder
stdout_logfile=/django/RestaurantOrder/log/websocket_out.log
stderr_logfile=/django/RestaurantOrder/log/websocket_err.log
autostart=true
autorestart=true
user=root
startsecs=10
file socket方式
区别于http socket
的为2处,1是nginx.conf
中的新增upstream websocket
,并在location /ws
中设置proxy_pass http://websocket;
,需要注意此处的http://
前缀不可省略;2是daphne
的启动方式改为daphne -u /django/RestaurantOrder/websocket.sock RestaurantOrder.asgi:application
。
nginx.conf:
upstream restaurant_order {
server unix:///django/RestaurantOrder/restaurant_order.sock;
}
upstream websocket {
server unix:///django/RestaurantOrder/websocket.sock;
}
server {
listen 8000;
server_name 114.116.25.246; # substitute your machine's IP address or FQDN
charset utf-8;
client_max_body_size 75M;
location /media {
alias /django/RestaurantOrder/media;
}
location /static {
alias /django/RestaurantOrder/static;
}
access_log /django/RestaurantOrder/log/access.log;
error_log /django/RestaurantOrder/log/error.log;
location / {
uwsgi_pass restaurant_order;
include /django/RestaurantOrder/uwsgi_params;
}
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;
proxy_read_timeout 36000s;
proxy_send_timeout 36000s;
}
}
supervisord.conf:
[program:restaurant_order_service]
command=uwsgi --ini /django/RestaurantOrder/restaurant_order_uwsgi.ini
directory=/django/RestaurantOrder
stdout_logfile=/django/RestaurantOrder/log/uwsgi_out.log
stderr_logfile=/django/RestaurantOrder/log/uwsgi_err.log
autostart=true
autorestart=true
user=root
startsecs=10
[program:restaurant_order_websocket]
command=/django/RestaurantOrder/environment/bin/daphne -u /django/RestaurantOrder/websocket.sock RestaurantOrder.asgi:application
directory=/django/RestaurantOrder
stdout_logfile=/django/RestaurantOrder/log/websocket_out.log
stderr_logfile=/django/RestaurantOrder/log/websocket_err.log
autostart=true
autorestart=true
user=root
startsecs=10