部署 – Django Channels:實現 WebSocket 異步通信,打造高效的長連接系統

A. 前言

Channels(ASGI)應用程式的部署與WSGI應用程式類似——您需要將它們加載到像Daphne這樣的伺服器中,並且可以根據需要調整伺服器進程的數量。對於Channels專案來說額外的一個可選需求是配置一個通道層。下面將介紹這兩個步驟。

B. 設定ASGI應用程式

如同在安裝路由一節中所討論的,您將有一個類似於 myproject/asgi.py 的文件,該文件將定義您的根應用程式。這幾乎可以確定是您的頂層(協議類型)路由器。

下面是一個可能的 asgi.py 文件範例:

import os

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
from django.core.asgi import get_asgi_application
from django.urls import path

os.environ.setdefault("DJANGO_SETTINGS_MODULE", "mysite.settings")
# Initialize Django ASGI application early to ensure the AppRegistry
# is populated before importing code that may import ORM models.
django_asgi_app = get_asgi_application()

from chat.consumers import AdminChatConsumer, PublicChatConsumer

application = ProtocolTypeRouter({
    # Django's ASGI application to handle traditional HTTP requests
    "http": django_asgi_app,

    # WebSocket chat handler
    "websocket": AllowedHostsOriginValidator(
        AuthMiddlewareStack(
            URLRouter([
                path("chat/admin/", AdminChatConsumer.as_asgi()),
                path("chat/", PublicChatConsumer.as_asgi()),
            ])
        )
    ),
})

C. 設定Channel後端服務

通常,頻道後端會連接到一個或多個作為通信層的中央伺服器。例如,Redis 後端會連接到一個 Redis 伺服器。所有這些設置都放在 CHANNEL_LAYERS 設定中;以下是一個遠端 Redis 伺服器的範例:

CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("redis-server-name", 6379)],
        },
    },
}

記得線透過指令(pip install -U channels_redis)安裝套件。

D. Run protocol servers

為了與外界溝通,您的 Channels/ASGI 應用程序需要加載至協議伺服器中。這些伺服器可以像 WSGI 伺服器一樣以 HTTP 模式運行您的應用程序,但它們也可以橋接至各種協議(例如聊天協議、物聯網(IoT)協議,甚至是無線電網路)。所有這些伺服器都有其自身的配置選項,但它們都有一個共同點 – 它們會要求您向其提供一個 ASGI 應用程序來運行。您只需將專案的 asgi.py 文件中的應用程序對象傳遞給協議伺服器作為其應運行的應用程序即可。

daphne -p 8001 myproject.asgi:application

E. HTTP and WebSocket

儘管 ASGI 是一個通用協定,我們無法涵蓋所有可能的伺服器,但很可能您會希望將 Channels 專案部署以便在 HTTP 和可能的 WebSocket 上運作,因此我們將更詳細地介紹這一點。

Channels 專案維護了一個官方的 ASGI HTTP/WebSocket 伺服器,名為 Daphne,我們將討論其配置。其他的 HTTP/WebSocket ASGI 伺服器也是可能的且只要遵循規範就能正常運作,但它們的配置可能會有所不同。

您可以選擇使用 Daphne 來處理所有的請求——無論是 HTTP 還是 WebSocket——或者如果您對穩定性較為保守,可以將標準的 HTTP 請求繼續通過 WSGI 伺服器運行,並僅使用 Daphne 來處理 WSGI 無法處理的事情,如 HTTP 長輪詢和 WebSockets。如果您選擇分開,則需要在 Daphne 和 WSGI 伺服器前放置某些東西來判斷哪些請求應發送到哪一個(可以使用 HTTP 路徑或網域),這裡沒有覆蓋這部分,但請知道您可以這麼做。

如果您將 Daphne 用於所有流量,它會自動協商處理 HTTP 和 WebSocket,因此不需要在不同的網域或路徑上配置 WebSocket(並且它們將能與常規檢視代碼共享 cookie,而如果您是按網域而不是路徑分開,這是做不到的)。

運行 Daphne 時,只需提供一個應用程序,就像需要為 WSGI 伺服器提供應用程序一樣。確保您有一個如上所述的 asgi.py 文件。

然後,您可以運行 Daphne 並將 ASGI 應用程序作為參數提供:

daphne myproject.asgi:application

你應該在一個進程管理器(如 systemd、supervisord)或容器編排系統(如 Kubernetes、Nomad)中運行 Daphne,以確保在需要時 Daphne 能夠重新啟動,並允許你調整進程的數量。如果你想要將多個 Daphne 實例綁定到機器上的同一個端口,使用能在端口上監聽並將文件描述符傳遞給啟動進程的進程管理器,然後通過 –fd NUM 傳遞文件描述符。你也可以指定 Daphne 綁定的端口和 IP:

daphne -b 0.0.0.0 -p 8001 myproject.asgi:application

你可以在 GitHub 上查看關於 Daphne 和其選項的更多資訊。

F. 其他的Web伺服器

您也可以選擇使用其他 ASGI 伺服器來服務 Django Channels。在某種程度上,ASGI 網頁伺服器應該是可以互換的,因為它們都具備基本的功能來處理 HTTP 和 WebSocket 請求。伺服器之間可能存在差異的方面包括其配置和預設值、效能特徵、資源限制的支援、不同的協議與套接字支援,以及程序管理的方法。您可以在 ASGI 實現的文檔中看到更多替代伺服器,例如 Uvicorn。

1. Uvicorn設定

Uvicorn使用方式與Daphne類似,步驟如下:

首先,確保您已經安裝了 Uvicorn。您可以使用 pip 來安裝,接著執行,就完成了。

pip3 install uvicorn
uvicorn myproject.asgi:application

其他用法如下

# 指定 Host 和 Port
uvicorn myproject.asgi:application --host 0.0.0.0 --port 8000

# 重新載入
uvicorn myproject.asgi:application --reload

# 設置Worker數量
uvicorn myproject.asgi:application --workers 4

G. 設定指引

這些是可能的設定範例——它們不保證能立即運行,應被視為指引而非直接的教學。

1. Nginx/Supervisor (Ubuntu)

此範例將在 Ubuntu 伺服器上設置一個 Django 站點,使用 Nginx 作為主要的網頁伺服器,並使用 supervisord 來運行和管理 Daphne。首先,安裝 Nginx 和 Supervisor:

sudo apt install nginx supervisor

現在,您需要創建一個 Supervisor 配置文件(通常位於 /etc/supervisor/conf.d/ 目錄中——這裡我們讓 Supervisor 監聽 TCP 埠,然後將該套接字傳遞給子進程,以便它們能夠共享同一綁定埠:

[fcgi-program:asgi]
# TCP socket used by Nginx backend upstream
socket=tcp://localhost:8000

# Directory where your site's project files are located
directory=/my/app/path

# Each process needs to have a separate socket file, so we use process_num
# Make sure to update "mysite.asgi" to match your project name
command=daphne -u /run/daphne/daphne%(process_num)d.sock --fd 0 --access-log - --proxy-headers mysite.asgi:application

# Number of processes to startup, roughly the number of CPUs you have
numprocs=4

# Give each process a unique name so they can be told apart
process_name=asgi%(process_num)d

# Automatically start and recover processes
autostart=true
autorestart=true

# Choose where you want your log to go
stdout_logfile=/your/log/asgi.log
redirect_stderr=true

為在 Supervisor 配置文件中引用的套接字創建運行目錄:

sudo mkdir /run/daphne/

當以不同的使用者運行 supervisor fcgi-program 時,請更改運行目錄的擁有者設定:

sudo chown <user>.<group> /run/daphne/

/run/ 資料夾在伺服器重啟時會被清空。要使 /run/daphne 資料夾保持持久,請建立一個檔案 /usr/lib/tmpfiles.d/daphne.conf,並將以下內容寫入該檔案:

d /run/daphne 0755 <user> <group>

讓 supervisor 重新閱讀並更新其作業:

sudo supervisorctl reread
sudo supervisorctl update

在命令行中運行帶有 –fd 0 的 daphne 命令會失敗,並導致錯誤 [Errno 88]:在非套接字上的套接字操作。

Supervisor 將自動創建套接字、綁定並在分叉群組中的第一個子進程之前進行監聽。該套接字將通過文件描述符編號 0(零)傳遞給每個子進程。請參閱 https://supervisord.org/configuration.html#fcgi-program-x-section-settings

接下來,必須告訴 Nginx 將流量代理到正在運行的 Daphne 實例:

upstream channels-backend {
    server localhost:8000;
}
...
server {
    ...
    location / {
        try_files $uri @proxy_to_app;
    }
    ...
    location @proxy_to_app {
        proxy_pass http://channels-backend;

        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;
    }
    ...
}

在配置 Nginx 以處理 WebSocket 升級請求時,有幾個重要的設定需要注意。首先,必須確保使用 HTTP/1.1 協議,因為 WebSocket 握手需要通過此協議進行。HTTP/1.0 版本不支持持久連接和 `Upgrade` 機制,因此需要在 proxy 配置中指定 `proxy_http_version 1.1;` 確保正確的協議版本。

接著,WebSocket 協議是透過 HTTP 請求的升級機制而實現的,這使得一個普通的 HTTP 連接可以轉變成 WebSocket 連接。為此,需要利用 `proxy_set_header Upgrade $http_upgrade;` 指令來告知 Nginx 傳遞 `Upgrade` 頭部,從而支持這種協議升級。

最後,還需要設置 `proxy_set_header Connection “upgrade”;`,這是為了告訴目標服務器(例如 Daphne),該連接應該始終保持在升級狀態。這與 `Upgrade` 頭部一起構成標準的 WebSocket 升級過程所必需的配置。這樣的設置確保了即使經過代理,WebSocket 連接仍然能夠正常工作。

重新載入 nginx 以套用更改:

sudo service nginx reload

H. 總結

Channels(ASGI)應用程式的部署類似於WSGI應用程式,主要需要將應用程式載入合適的伺服器,例如Daphne,同時可以透過調整伺服器進程數量來進行擴展。此外,Channels專案需要配置一個通道層,常見的是連接到Redis伺服器。要運行Channels/ASGI應用程式,必須使用像Daphne這樣的協議伺服器,並可以使用Nginx和Supervisor在Ubuntu環境中進行管理和配置。在擁有協議伺服器後,您還可以根據需要處理HTTP及WebSocket請求。確保正確設置Nginx來代理至Daphne實例,並使用supervisor控制進程重啟及任務管理。 提供了完整的Nginx和Supervisor設置範例以供參考。

I. 系列文章