Django-Channels作用
在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(消費者)。
Django-Channels使用
本文基於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/<slug:table_id>/', 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/templates/chat/room.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Chat Room</title>
</head>
<body>
<textarea id="chat-log" cols="100" rows="20"></textarea><br/>
<input id="chat-message-input" type="text" size="100"/><br/>
<input id="chat-message-submit" type="button" value="Send"/>
</body>
<script>
var roomName = {{ room_name_json }};
var chatSocket = new WebSocket('ws://' + window.location.host + '/ws/table/' + roomName + '/');
chatSocket.onmessage = function (e) {
var data = JSON.parse(e.data);
document.querySelector('#chat-log').value += (JSON.stringify(data) + '\n');
};
chatSocket.onclose = function (e) {
console.error('Chat socket closed unexpectedly');
};
document.querySelector('#chat-message-input').focus();
document.querySelector('#chat-message-input').onkeyup = function (e) {
if (e.keyCode === 13) { // enter, return
document.querySelector('#chat-message-submit').click();
}
};
document.querySelector('#chat-message-submit').onclick = function (e) {
var messageInputDom = document.querySelector('#chat-message-input');
var message = messageInputDom.value;
chatSocket.send(JSON.stringify(message));
messageInputDom.value = '';
};
</script>
</html>
最終效果:
Django-Channels部署
在官方文檔中推薦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