Django-Channels使用和部署

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(消費者)。

玩轉 ASGI:從零到一實現一個實時博客

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-Channelshttp部分和websocket部分均使用daphne進行部署,該方法參見DjangoChannels Docs

本文使用的方法爲使用Nginx代理,將http部分請求發送給uwsgi進行處理,將websocket部分請求發送給daphne進行處理。uwsgidaphhe均使用supervisord進行控制。

需要注意的是,由於Nginx無法識別http請求和websocket請求,需要通過路由來區分是哪種協議。我使用的方法是規定所有的websocket的路由均以/ws開頭(如: ws://www.example/ws/table/table_id/),這樣就可以讓Nginx將所有以/ws開頭的請求全部轉發給daphne進行處理。

Nginxdaphne進行通信時,有http socketfile 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
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章