Django Channels 入門指南

http://www.oschina.NET/translate/in_deep_with_django_channels_the_future_of_real_time_apps_in_djangohtml

 

今天,咱們很高興請到Jacob Kaplan-Moss。Jacob是來自Herokai,也是 Django的長期的核心代碼貢獻者,他將在這裏分享一些他對某些特性的深刻研究,他認爲這些特性將從新定義框架將來。前端

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

到如今2016年,網絡明顯更增強大。過去的幾年裏已經看到了所謂的「實時」網絡應用:在這類應用中客戶端和服務器之間、點對點通訊交互很是頻繁。包含不少服務(又名微服務)的應用也變成是常態。新的web技術容許web應用程序走向十年前咱們只敢在夢裏想象的方向。這些核心技術之一就是WebSockets:一種新的提供全雙工通訊的協議——一個持久的,容許任什麼時候間發送數據的客戶端和服務器之間的鏈接。mysql

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

這在WebSockets中是行不通的 !視圖的生命週期只在一個請求當中,沒有一種機制能打開一個鏈接不斷的發送數據到客戶端,而不發送相關請求。github

所以:Django  Channels就應運而生了。Channels,簡而言之,取代了Django中的「guts」 ——請求/響應週期發送跨通道的消息。Channels容許Django以很是相似於傳統HTTP的方式支持WebSockets。Channels也容許在運行Django的服務器上運行後臺任務。HTTP請求表現之前同樣,但也經過Channels進行路由。所以,在Channels 支持下Django如今看起來像這樣:web

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

Channels基本上就是任務隊列:消息被生產商推到通道,而後傳遞給監聽通道的消費者之一。若是你使用Go語言中的渠道,這個概念應該至關熟悉。主要的區別在於,Django Channels經過網絡工做,使生產者和消費者透明地運行在多臺機器上。這個網絡層稱爲通道層。通道設計時使用Redis做爲其首選通道層,雖然也支持其餘類型(和API來建立自定義通道層)。有不少整潔和微妙的技術細節,查閱文檔能夠看到完整的記錄。sql

如今,通道做爲一個獨立的應用程序搭配使用Django 1.9使用。計劃是將通道合併到Django1.10版本,今年夏天將會發布。shell

我認爲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):

?
1
2
3
4
5
6
7
8
9
class  Room(models.Model):
     name  =  models.TextField()
     label  =  models.SlugField(unique = True )
 
class  Message(models.Model):
     room  =  models.ForeignKey(Room, related_name = 'messages' )
     handle  =  models.TextField()
     message  =  models.TextField()
     timestamp  =  models.DateTimeField(default = timezone.now, db_index = True )

(在這一步中,包括後面的例子,我已經將代碼最簡化,但願能將焦點放到重點上,所有代碼請看Gitbub。)

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

?
1
2
3
4
5
6
7
8
9
10
11
12
def  chat_room(request, label):
     # If the room with the given label doesn't exist, automatically create it
     # upon first visit (a la etherpad).
     room, created  =  Room.objects.get_or_create(label = label)
 
     # We want to show the last 50 messages, ordered most-recent-last
     messages  =  reversed (room.messages.order_by( '-timestamp' )[: 50 ])
 
     return  render(request,  "chat/room.html" , {
         'room' : room,
         'messages' : messages,
     })

如今,咱們已經已經有了一個能夠運行的Django應用。若是你在標準的Django環境中運行它,你能夠看到已經存在的聊天室和聊天記錄,可是聊天室內沒法進行交互操做。實時沒有起做用,咱們得作工做來處理 WebSockets。

接下來咱們作什麼

爲了搞明白接下來後臺須要作些什麼,咱們得先看下客戶端的代碼。你能夠在 chat.js 中找到,其實也沒作多少工做!首先,建立一個 websocket:

?
1
2
var ws_scheme  =  window.location.protocol  = =  "https:"  "wss"  "ws" ;
var chat_socket  =  new ReconnectingWebSocket(ws_scheme  +  '://'  +  window.location.host  +  "/chat"  +  window.location.pathname);

注意:

接下來,咱們將加入一個回調函數,當表單提交時,咱們就經過WebSocket發送數據(而不是 POST數據):

?
1
2
3
4
5
6
7
8
$( '#chatform' ).on( 'submit' , function(event) {
     var message  =  {
         handle: $( '#handle' ).val(),
         message: $( '#message' ).val(),
     }
     chat_socket.send(JSON.stringify(message));
     return  false;
});

咱們能夠經過WebSocket發送任何想要發送的數據。像衆多的API同樣, JSON 是最容易的,因此咱們將要發送的數據打包成JSON格式。

最後,咱們須要將回調函數與WebSocket上的新數據接收事件對接起來:

?
1
2
3
4
5
6
7
8
chatsock.onmessage  =  function(message) {
     var data  =  JSON.parse(message.data);
     $( '#chat' ).append( '<tr>' 
         +  '<td>'  +  data.timestamp  +  '</td>' 
         +  '<td>'  +  data.handle  +  '</td>'
         +  '<td>'  +  data.message  +  ' </td>'
     +  '</tr>' );
};

簡單提示:從獲取的信息中拉取數據,在會話的表上加上一行。若是如今就運行這個代碼,他是沒法運行的,如今尚未誰監聽WebSocket鏈接呢,只是簡單的HTTP。如今,讓咱們來鏈接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配置中定義通道層:

?
1
2
3
4
5
6
7
8
9
CHANNEL_LAYERS  =  {
     "default" : {
         "BACKEND" "asgi_redis.RedisChannelLayer" ,
         "CONFIG" : {
             "hosts" : [os.environ.get( 'REDIS_URL' 'redis://localhost:6379' )],
         },
         "ROUTING" "chat.routing.channel_routing" ,
     },
}

要注意的是咱們把Redis的鏈接URL放到環境外面,以適應部署到Heroku的狀況。

3. 通道路由

在通道層(CHANNEL_LAYERS),咱們已經告訴 Channel去哪裏找通道路由——chat.routing.channel_routing。通道路由很相似與URL路由的概念:URL路由將URL映射到視圖函數;通道路由將通道映射到消費者函數。跟 urls.py相似,按照慣例通道路由應該在routing.py裏。如今,咱們建立一條空路由:

?
1
channel_routing = {}

(咱們將在後面看到好幾條通道路由信息,當鏈接WebSocket的時候回用到。)

你會注意到咱們的app裏有urls.py和routing.py兩個文件:咱們使用同一個app處理HTTP請求和WebSockets。這是很典型的作法:Channels應用也是Django應用,因此你想用的全部Django的特性——視圖,表單,模型等等——均可以在Channels應用裏使用。

4. 運行

最後,咱們須要替換掉Django的基於HTTP/WSGI的請求處理器,而是使用通道。它是一個基於新興標準ASGI(異步服務器網關接口)的, 因此咱們將在asgi.py文件裏定義處理器:

?
1
2
3
4
5
import  os
import  channels.asgi
 
os.environ.setdefault( "DJANGO_SETTINGS_MODULE" "chat.settings" )
channel_layer  =  channels.asgi.get_channel_layer()

(未來,Django會自動生成這個文件,就像如今自動生成wsgi.py文件同樣。)

如今,若是一切順利的話,咱們應該能在通道上把這個app運行起來。Channels接口服務叫作Daphne,咱們能夠運行以下命令運行這個app:

?
1
$ daphne chat.asgi:channel_layer  - - port  8888

** 若是如今訪問http://localhost:8888/ 咱們會看到……什麼事情也沒發生。這很讓人困惑,直到你想起Channels將 Django分紅了兩部分:前臺接口服務 Daphne,後臺消息消費者。因此想要處理HTTP 請求,咱們得運行一個worker:

?
1
$ python manage.py runworker

如今請求應該能傳遞過去了。這說明了其中的機制很簡潔: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:

?
1
2
3
4
5
6
7
from  import  consumers
 
channel_routing  =  {
     'websocket.connect' : consumers.ws_connect,
     'websocket.receive' : consumers.ws_receive,
     'websocket.disconnect' : consumers.ws_disconnect,
}

其實很簡單:就是將每一個通道鏈接到對應的處理函數。如今咱們來看看這些函數。按照慣例咱們會將這些函數放到一個 consumers.py 文件裏(可是像視圖同樣,其實也能夠放在任何地方)。

首先來看看 ws_connect:

?
1
2
3
4
5
6
7
8
9
10
from  channels  import  Group
from  channels.sessions  import  channel_session
from  .models  import  Room
 
@channel_session
def  ws_connect(message):
     prefix, label  =  message[ 'path' ].strip( '/' ).split( '/' )
     room  =  Room.objects.get(label = label)
     Group( 'chat-'  +  label).add(message.reply_channel)
     message.channel_session[ 'room' =  room.label

(爲了清晰起見,我將代碼中的異常處理和日誌去掉了。要看完整版本,請看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上每接收一條消息,這個消費者都會被調用:

?
1
2
3
4
5
6
7
@channel_session
def  ws_receive(message):
     label  =  message.channel_session[ 'room' ]
     room  =  Room.objects.get(label = label)
     data  =  json.loads(message[ 'text' ])
     =  room.messages.create(handle = data[ 'handle' ], message = data[ 'message' ])
     Group( 'chat-' + label).send({ 'text' : json.dumps(m.as_dict())})

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

最初的幾行很簡單:從 channel_session中解析出聊天室,在數據庫中查找出來該聊天室,解析JSON消息,將消息做爲Message對象存放在數據庫中。而後,咱們所要做的就是將這條消息廣播給聊天室裏全部的成員,爲了作到這點咱們可使用和前面同樣的通道組。Group.send()將會把這條信息發送到加入到本組的全部reply_channel。

而後, ws_disconnect就很簡單了:

?
1
2
3
4
@channel_session
def  ws_disconnect(message):
     label  =  message.channel_session[ 'room' ]
     Group( 'chat-' + label).discard(message.reply_channel)

這裏,在從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:

?
1
2
web: daphne chat.asgi:channel_layer --port $PORT --bind 0.0.0.0 -v2
worker: python manage.py runworker -v2

當咱們首次部署,咱們須要確認兩種處理類型都在運行中(Heroku默認值啓動web進程):

?
1
$ heroku  ps :scale web=1: free  worker=1: free

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

2. 插件: Postgres和Redis

就像Django的大多數應用,你須要一個數據庫, Heroku的Postgres能夠完美的知足要求。然而,Channels也須要一個 Redis實例做爲通道層。因此,咱們在首次部署咱們的應用時須要建立一個 Heroku Postgres和一個 Heroku Redis:

$ heroku addons:create heroku-postgresql
$ heroku addons:create 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。我推薦其中的幾篇特別好的文章:

相關文章
相關標籤/搜索