實時 Django 終於來了 —— Django Channels 入門指南

發表於2016-11-27

今天,我們很高興請到Jacob Kaplan-Moss。Jacob是來自Herokai,也是 Django的長期的核心程式碼貢獻者,他將在這裡分享一些他對某些特性的深入研究,他認為這些特性將重新定義框架未來。

當Django剛建立時,那是十多年前,網路還是一個不太複雜的地方。大部分的網頁都是靜態的。由資料庫支撐的模型/檢視/ 控制器架構的網路應用還是很新鮮的東西。Ajax剛剛開始被使用,只在較少的場景中。

到現在2016年,網路明顯更加強大。過去的幾年裡已經看到了所謂的“實時”網路應用:在這類應用中客戶端和伺服器之間、點對點通訊互動非常頻繁。包含很多服務(又名微服務)的應用也變成是常態。新的web技術允許web應用程式走向十年前我們只敢在夢裡想象的方向。這些核心技術之一就是WebSockets:一種新的提供全雙工通訊的協議——一個持久的,允許任何時間傳送資料的客戶端和伺服器之間的連線。

在這個新的世界,Django顯示出了它的老成。在其核心,Django是建立在請求和響應的簡單概念之上的:瀏覽器發出請求,Django呼叫一個檢視,它返回一個響應併傳送回瀏覽器。

這在WebSockets中是行不通的 !檢視的生命週期只在一個請求當中,沒有一種機制能開啟一個連線不斷的傳送資料到客戶端,而不傳送相關請求。

因此:Django  Channels就應運而生了。Channels,簡而言之,取代了Django中的“guts” ——請求/響應週期傳送跨通道的訊息。Channels允許Django以非常類似於傳統HTTP的方式支援WebSockets。Channels也允許在執行Django的伺服器上執行後臺任務。HTTP請求表現以前一樣,但也通過Channels進行路由。因此,在Channels 支援下Django現在看起來像這樣:

如您所見,Django Channels引入了一些新的概念:

Channels基本上就是任務佇列:訊息被生產商推到通道,然後傳遞給監聽通道的消費者之一。如果你使用Go語言中的渠道,這個概念應該相當熟悉。主要的區別在於,Django Channels通過網路工作,使生產者和消費者透明地執行在多臺機器上。這個網路層稱為通道層。通道設計時使用Redis作為其首選通道層,雖然也支援其他型別(和API來建立自定義通道層)。有很多整潔和微妙的技術細節,查閱文件可以看到完整的記錄。

現在,通道作為一個獨立的應用程式搭配使用Django 1.9使用。計劃是將通道合併到Django1.10版本,今年夏天將會發布。

我認為Channels將是Django的一個非常重要的外掛:它們將支撐Django順利進入這個新的web開發的時代。雖然這些api還沒有成為Django的一部分,他們將很快就會是!所以,現在是一個完美的時間開始學習Channels:你可以瞭解未來的Django。

開始實踐:如何在Django中實現一個實時聊天應用

作為一個例子,我構建了一個簡單的實時聊天應用程式——就像一個非常非常輕量級的Slack。有很多的房間,每個人都在同一個房間裡可以聊天,彼此實時互動(使用WebSockets)。

你可以訪問我在網路上部署的例子,看看在GitHub上的程式碼,或點選這個按鈕來部署自己的。(這需要一個免費的Heroku賬戶,所以得要先註冊):

注意:你需要在點選前面的連結後,啟動工作程式。使用儀表盤或執行heroku ps:scale web=1:free worker=1:free。

如果你想深入瞭解這個應用程式是如何工作的——包括你為什麼需要worker!——那麼請繼續讀下去。我將會一步一步來構建這個應用程式,並突出關鍵位置和概念。

第一步——從Django開始

雖然在實現上有了很大差異,但是這仍舊是我們使用了十年的Django。所以第一步和其他任何Django應用是一樣的(如果你是Django新手,你得看看如何在Heroku上開始使用PythonDjango新手教程)。建立一個工程後,你可以定義模型來表示一個聊天室和其中的訊息(chat/models.py):

(在這一步中,包括後面的例子,我已經將程式碼最簡化,希望能將焦點放到重點上,全部程式碼請看Gitbub。)

然後建立一個聊天室檢視以及相應的urls.py模板

現在,我們已經已經有了一個可以執行的Django應用。如果你在標準的Django環境中執行它,你可以看到已經存在的聊天室和聊天記錄,但是聊天室內無法進行互動操作。實時沒有起作用,我們得做工作來處理 WebSockets。

接下來我們做什麼

為了搞明白接下來後臺需要做些什麼,我們得先看下客戶端的程式碼。你可以在 chat.js 中找到,其實也沒做多少工作!首先,建立一個 websocket:

注意:

接下來,我們將加入一個回撥函式,當表單提交時,我們就通過WebSocket傳送資料(而不是 POST資料):

我們可以通過WebSocket傳送任何想要傳送的資料。像眾多的API一樣, JSON 是最容易的,所以我們將要傳送的資料打包成JSON格式。

最後,我們需要將回撥函式與WebSocket上的新資料接收事件對接起來:

安裝和建立 Channels

要將這個應用“通道化”,我們需要做三件事情:安裝Channels,建立通道層,定義通道路由,修改我們的工程使其執行在Channels上(而不是WSGI)。

1. 安裝Channels

要安裝Channels,只需要執行pip install channels,然後將 “channels”新增到 INSTALLED_APPS配置項中。安裝Channels後,允許Django以“通道模式”執行,使用上面描述的通道架構來完成請求/響應的迴圈。(為了向後相容,你仍可以以 WSGI模式執行Django ,但是在這種模式下WebSockets和Channel的其他特性就不能工作了。)

2. 選擇一個通道層

接下來,我們將定義一個通道層。這是Channels用來在消費者和生產者(訊息傳送者)之間傳遞訊息的交換機制。 這是一種有特定屬性的訊息佇列(詳細資訊請檢視Channels文件)。

我們將使用Redis作為我們的通道層:它是首選的生產型(可用於工程部署)通道層,是部署在Heroku上顯而易見的選擇。 當然也有一些駐留記憶體和基於資料的通道層,但是它們更適合於本地開發或者低流量情況下使用。 (更多細節,再次請檢視 文件。)

但是首先:因為Redis通道層是在另外的包中實現的,我們需要執行pip安裝 asgi_redis。(我將會在下面稍微介紹點“ASGI”。)然後我們在CHANNEL_LAYERS配置中定義通道層:

要注意的是我們把Redis的連線URL放到環境外面,以適應部署到Heroku的情況。

3. 通道路由

在通道層(CHANNEL_LAYERS),我們已經告訴 Channel去哪裡找通道路由——chat.routing.channel_routing。通道路由很類似與URL路由的概念:URL路由將URL對映到檢視函式;通道路由將通道對映到消費者函式。跟 urls.py類似,按照慣例通道路由應該在routing.py裡。現在,我們建立一條空路由:

(我們將在後面看到好幾條通道路由資訊,當連線WebSocket的時候回用到。)

你會注意到我們的app裡有urls.py和routing.py兩個檔案:我們使用同一個app處理HTTP請求和WebSockets。這是很典型的做法:Channels應用也是Django應用,所以你想用的所有Django的特性——檢視,表單,模型等等——都可以在Channels應用裡使用。

4. 執行

最後,我們需要替換掉Django的基於HTTP/WSGI的請求處理器,而是使用通道。它是一個基於新興標準ASGI(非同步伺服器閘道器介面)的, 所以我們將在asgi.py檔案裡定義處理器:

 (將來,Django會自動生成這個檔案,就像現在自動生成wsgi.py檔案一樣。)

現在,如果一切順利的話,我們應該能在通道上把這個app執行起來。Channels介面服務叫做Daphne,我們可以執行如下命令執行這個app:

** 如果現在訪問http://localhost:8888/ 我們會看到……什麼事情也沒發生。這很讓人困惑,直到你想起Channels將 Django分成了兩部分:前臺介面服務 Daphne,後臺訊息消費者。所以想要處理HTTP 請求,我們得執行一個worker:

現在請求應該能傳遞過去了。這說明了其中的機制很簡潔:Channels 繼續處理 HTTP(S)請求,但是是以一個完全不同的方式去處理,這與通過Django執行 Celery 沒有太大的不同,那種情況下執行WSGI服務的同時也要執行Celery服務。不過現在,所有的任務——HTTP請求, WebSockets,後臺服務都在worker中執行起來了.

(順便說一句,我們仍然可以通過執行python manage.py runserver命令來做本地測試。當這麼做時, Channels只是在同一程式裡執行起Daphne和一個worker。)

WebSocket消費者

好了,我們已經完成了安裝;讓我們開始進入最奇妙的部分吧。

Channels 將WebSocket連線對映到三個通道中:

  • 一個新的客戶端 (如瀏覽器)第一次通過WebSocket連線上時,一條訊息被髮送到 websocket.connect 通道。當這發生時,我們記錄這個客戶端當前進入一個已知的聊天室。
  • 每條客戶端通過已建立的socket傳送的訊息都被髮送到 websocket.receive通道。(這些都是從瀏覽器接收到的訊息;記住通道都是單向的。我們等一會兒會介紹如何將訊息傳送給客戶端。)當一條訊息被接受時,我們將對聊天室裡所有其他客戶端進行廣播。
  • 最後,當客戶端斷開連線時,一條訊息被髮送到websocket.disconnect通道。當這發生時,我們將此客戶端從聊天室裡移除。

首先,我們得在routing.py檔案裡對這個三個通道進行hook:

其實很簡單:就是將每個通道連線到對應的處理函式。現在我們來看看這些函式。按照慣例我們會將這些函式放到一個 consumers.py 檔案裡(但是像檢視一樣,其實也可以放在任何地方)。

首先來看看 ws_connect:

(為了清晰起見,我將程式碼中的異常處理和日誌去掉了。要看完整版本,請看GitHub上的consumers.py)。

這裡程式碼很多,讓我們一行行來看:

7. 客戶端將會連線到一個/chat/{label}/形式的WebSocket,label對映的是一個房間的屬性。因為所有的WebSocket訊息(不考慮URL)客戶端都可以在相同的頻道里傳送和獲取訊息,我們要在哪個房間工作,通過路徑解析就可以。

客戶端解析WebSocket路徑是通過讀取message[‘path’]獲得的,這不同於傳統的URL路由,Django的urls.py的路由是基於path的。如果你有多個WebSocket URL,你會需要路由到你自己定製的不同函式。(這是一個“早期”頻道方面的內容;很可能在未來的版本里Channel將會包含在WebSocket URL 路由中。)

8. 現在,我們可以從資料庫中檢視Room物件了。

9. 這條線是使聊天功能能工作的關鍵。我們需要知道如何把訊息傳送回這個客戶端。要做到這點,我們將使用訊息的應答通道——每條訊息都會有一個應答通道屬性(reply_channelattribute),可以用來把訊息傳送回這個客戶端。(我們不需要去自己建立這個通道;Channels已經建立好了。)

然而,只把訊息傳送到這一個通道還是遠遠不夠的的;當一個使用者聊天時,我們想把訊息送給每一個連線到此聊天室的使用者。要做到這點,我們使用一個通道組(channel group)。一個組是由多個通道連線而成,你可以用他來廣播訊息。所以,我們將這個訊息的應答通道加入到這個聊天室的特殊通道組中。

10. 最後,後續的訊息(接收/斷開)不再包含這個URL(因為連線已經啟用)。所以,我們需要一種方式來把一個WebSocket連線對映到哪個聊天室記錄下來。要做到這點,我們可以使用一個通道會話。通道會話很像 Django的會話框架: 它們通過通道訊息的屬性message.channel_session把這些資訊持久化下來。我們給一個消費者新增修飾屬性 @channel_session,就可以讓會話框架起效。 (文件見 通道會話如何工作的更多細節)。

現在一個客戶端已經連線上來了,讓我們看看ws_receive。WebSocket上每接收一條訊息,這個消費者都會被呼叫:

(再一次說明,為了清晰起見,我把錯誤處理和日誌都去掉了。)

最初的幾行很簡單:從 channel_session中解析出聊天室,在資料庫中查詢出來該聊天室,解析JSON訊息,將訊息作為Message物件存放在資料庫中。然後,我們所要作的就是將這條訊息廣播給聊天室裡所有的成員,為了做到這點我們可以使用和前面一樣的通道組。Group.send()將會把這條資訊傳送到加入到本組的所有reply_channel。

然後, ws_disconnect就很簡單了:

這裡,在從channel session裡查詢到聊天室後,我們從聊天組裡斷開了reply_channel,就是這樣!

部署和擴充套件

現在我們已經把 WebSockets連線起來並開始工作,我們可以像上面一樣執行daphne和worker進行測試,或者執行manage.py runserver)。但是和自己聊天是很寂寞的哦,所以讓我們在Heroku上把它跑起來!

大部分情況下, 一個 Channels 應用和一個Python應用在Heroku上都是一樣的——在requirements.txt中有詳細需求, 在runtime.txt定義Python執行事,通過標準的git推送到heroku上進行部署,等等。 (對於一個新手,請看 在Heroku上開始Python開發教程。) 我將重點突出那些Channel應用和標準Django應用不一樣的地方:

1. Procfile 和處理型別

因為Channels應用同時需要 HTTP/WebSocket 服務和一個後臺通道消費者, 所以Procfile需要定義這兩種型別。下面是我們的Procfile:

 當我們首次部署,我們需要確認兩種處理型別都在執行中(Heroku預設值啟動web程式):

(一個簡單的應用將執行在 Heroku的免費或者愛好者層上,不過在實際使用環境中你可能需要升級到產品級來提高吞吐量。)

2. 外掛: Postgres和Redis

就像Django的大多數應用,你需要一個資料庫, Heroku的Postgres可以完美的滿足要求。然而,Channels也需要一個 Redis例項作為通道層。所以,我們在首次部署我們的應用時需要建立一個 Heroku Postgres和一個 Heroku Redis:

3. 擴充套件

因為Channels實在是太新了,擴充套件性問題還不是很瞭解。然而,基於現在的架構和我早前做的一些效能測試,我可以做出一些預測。關鍵點在於Channels 把負責連線的處理程式(daphne)和負責通道訊息處理的處理程式(runworker)分開了。這意味著:

  • 通道的吞吐量——HTTP請求, WebSocket訊息,或者自定義的通道訊息——取決於工作者程式的數量。所以,如果你需要處理大量的請求,你可以擴充套件工作者程式 (比如,heroku上 ps:scale worker=3)。
  • 併發水平——當前開啟的連線數——將受限於前端web程式的規模。所以,如果你需要處理大量併發的WebSocket連線,你得擴充套件web程式(比如, heroku 上ps:scale worker=2)。

基於我前期做的測試工作, 在一個Standard-1X程式內Daphne是非常適合處理成百的併發連線的。所以我估計很少有場景需要擴充套件這個web程式。一個Channels應用中的工作者程式的個數與一個老風格Django應用所需的web程式個數是相當的。

接下來要做些什麼呢?

對WebSocket的支援是Django的一項很大的新特性,但是這隻粗淺介紹了Channels可以做些什麼。你要記住:Channels是一個執行後臺任務的通用工具。因此,很多過去需要 Celery 或者 Python-RQ 才能做得事情,都可以用Channels替換。 Channels無法完全替換複雜的任務佇列:他有些很重要的限制,比如只發一次,這並不適合所有的場景。 檢視文件以瞭解全部細節。 當然, Channels可以使通常的後臺任務更加簡單。比如,你可以很容易的使用Channels完成影像縮圖生成,傳送郵件、推文或者簡訊,執行耗時資料計算等等工作。

對於Channels來說:計劃在 Django 1.10中包含Channels ,定於今年夏天釋出。這意味著現在是一個很好的時機來嘗試一下並給出反饋:您的反饋將會推動這一重要特性的發展方向。如果你想參與進來,看看這份指導文件向Djang貢獻程式碼,  然後到 django開發者郵件列表 裡分享你的反饋。

最後: 非常感謝 Andrew Godwin 在 Channels上付出的努力工作。這真是Django的一個非常激動人心的新方向,我很激動地看到它開始發展起來。

進一步閱讀

關於Channels的更多資訊,請檢視Channels文件,其中包含很多細節和引用,包括:

關於在 Heroku上使用Python 的資訊,請訪問Python on Heroku in Dev Center。我推薦其中的幾篇特別好的文章:

相關文章