Channels概念

Django的傳統(tǒng)概念圍繞著HTTP請求和響應(yīng)展開:服務(wù)器收到一個請求,Django調(diào)起為其服務(wù),生成響應(yīng)并發(fā)送,然后Django離開、等著下一個請求。

當(dāng)互聯(lián)網(wǎng)只包含簡單的瀏覽器行為時,這沒什么問題。但現(xiàn)代網(wǎng)絡(luò)包含了WebSocket和HTTP 2服務(wù)器推送等功能,允許網(wǎng)站在傳統(tǒng)請求/響應(yīng)循環(huán)之外進(jìn)行其他通信。

除此之外,還有許多非關(guān)鍵的任務(wù),需要應(yīng)用程序?qū)⑵浞职l(fā)出去,在響應(yīng)發(fā)送后繼續(xù)處理——比如保存緩存,或?yàn)樾律蟼鞯膱D像生成縮略圖。

Channels將Django的運(yùn)行方式改為“面向事件”——不僅僅響應(yīng)請求,而是響應(yīng)通道中發(fā)送的大量事件。這里仍然沒有持久狀態(tài)——每個事件處理程序,也可以叫事件消費(fèi)者,都是以獨(dú)立的方式調(diào)用的,就像調(diào)用視圖一樣。

讓我們先看看通道是什么。

通道是什么?

理所當(dāng)然,核心就是叫做通道的數(shù)據(jù)結(jié)構(gòu)。通道是什么?它是一個有序的、先進(jìn)先出的隊(duì)列,消息會過期,最多發(fā)一次,一次只發(fā)給一個監(jiān)聽?wèi)?yīng)用

可以將其類比為任務(wù)隊(duì)列——生產(chǎn)者將消息發(fā)送到一個通道,再發(fā)給一個監(jiān)聽該通道的消費(fèi)者。

最多發(fā)一次是指,一條消息要么有一個消費(fèi)者收到,要么沒人收到(例如通道突然崩潰了)。與之對應(yīng)的是至少一次:正常情況下,一條消息有一個消費(fèi)者收到,但程序崩潰時,它極有事后可能再重新發(fā)送,因此還會有其他消費(fèi)者重復(fù)收到。后者不是我們想要的。

還有其他幾個限制——消息必須是可序列化的類型,并且必須保持在一定的大小限制之內(nèi)——但是在接觸更高級的用法之前,無需擔(dān)心這些實(shí)現(xiàn)細(xì)節(jié)。

通道有容量,即便沒有消費(fèi)者,生產(chǎn)者也可以先向通道中寫入大量消息,消費(fèi)者稍后出現(xiàn)再接收隊(duì)列中的消息。

如果你使用過Go通道:Go通道與Django通道相當(dāng)像,關(guān)鍵區(qū)別在于Django通道是網(wǎng)絡(luò)透明的,在不同進(jìn)程甚至不同計(jì)算機(jī)上運(yùn)行的生產(chǎn)者和消費(fèi)者,都可以通過網(wǎng)絡(luò)訪問到我們的通道。

在網(wǎng)絡(luò)中,我們通過名稱字符串來唯一地標(biāo)識通道——任何計(jì)算機(jī)可以將消息發(fā)送到任何命名通道,只要它們接入了同一個通道后端。例如兩臺不同的計(jì)算機(jī)都寫入叫做“http.request”的通道,他們寫入的就是同一個通道。

如何使用通道?

那么Django如何使用這些通道呢?在Django中,您可以編寫一個函數(shù)來使用通道:

def my_consumer(message):
    pass

然后在通道路由中為其分配一個通道:

channel_routing = {
    "some-channel": "myapp.consumers.my_consumer",
}

該通道每收到一條消息,Django都調(diào)用這個消費(fèi)者函數(shù),并傳入message參數(shù)。message帶有content、channel和其他一些屬性,其中content屬性一定是字典(dict)類型,channel屬性用來標(biāo)明發(fā)送消息的通道名稱。

Channels將Django從傳統(tǒng)的請求/響應(yīng)模式,改為工作進(jìn)程模式:監(jiān)聽所有分配了消費(fèi)者的通道,收到消息就運(yùn)行對應(yīng)的消費(fèi)者?,F(xiàn)在,Django不只是在一個連接到WSGI服務(wù)器的進(jìn)程中運(yùn)行,而是在三個獨(dú)立的層中運(yùn)行:

  • 接口服務(wù)器,用于Django與外部世界之間的通信。這包括一個WSGI適配器以及一個單獨(dú)的WebSocket服務(wù)器——這在運(yùn)行接口服務(wù)器中進(jìn)行了介紹。
  • 通道后端,其中包括可擴(kuò)展的Python代碼,以及用來傳輸消息的數(shù)據(jù)存儲機(jī)制(例如Redis或共享內(nèi)存段)。
  • 工作進(jìn)程,監(jiān)聽所有相關(guān)的通道,并在消息就緒時運(yùn)行消費(fèi)者代碼。

這或許看起來很簡單,但我們就是這樣計(jì)劃的:與其嘗試完全異步的架構(gòu),不如將現(xiàn)有的Django視圖再稍微復(fù)雜抽象一點(diǎn)。

A view takes a request and returns a response; a consumer takes a channel message and can write out zero to many other channel messages.

現(xiàn)在,讓我們建立一個請求通道(取名http.request),以及面向單個客戶端的響應(yīng)通道(例如http.response.o4f2h2fd),請求通道中的消息帶有一個reply_channel屬性,對應(yīng)響應(yīng)通道的名稱。這樣,消息消費(fèi)者就很類似一個視圖:

# 監(jiān)聽http.request
def my_consumer(message):
    # 將請求信息從message中解碼,生成Request對象
    django_request = AsgiRequest(message)
    # 運(yùn)行視圖
    django_response = view(django_request)
    # 將響應(yīng)編碼為message格式
    for chunk in AsgiHandler.encode_response(django_response):
        message.reply_channel.send(chunk)

這就是通道的機(jī)制。接口服務(wù)器將外部連接(HTTP、WebSocket等)轉(zhuǎn)換為通道中的消息,你編寫工作進(jìn)程來處理這些消息。正常情況下,普通的HTTP請求還是交給Django內(nèi)置的消費(fèi)者,后者將請求傳入視圖/模板系統(tǒng),但如果需要,也可以重寫它以添加功能。

關(guān)鍵的部分在于,你可以運(yùn)行代碼來響應(yīng)任何事件——包括你自己創(chuàng)建的事件,運(yùn)行的代碼還可以進(jìn)一步發(fā)消息。你可以在保存Model、其他Views和Forms的代碼中觸發(fā)事件。這樣可以方便的寫推送程序,例如使用WebSocket或HTTP長輪詢來實(shí)時通知客戶端(比如聊天信息,或者Admin實(shí)時查看其他用戶編輯更新的內(nèi)容)。

通道類型

通道主要有兩種用途。第一種顯而易見,就是把工作分配給消費(fèi)者——通道中新增一條消息,任何一個工作進(jìn)程都可以接收消息并運(yùn)行消費(fèi)者。

第二種通道用于響應(yīng)(HTTP、WebSocket)請求。值得注意的是,只有接口服務(wù)器才會監(jiān)聽這種通道的消息。每個響應(yīng)通道名字都不一樣,并且必須路由回到特定的接口服務(wù)器。

兩者區(qū)別不大——它們都符合通道的核心定義——但當(dāng)服務(wù)器集群擴(kuò)大規(guī)模時會有問題。對于第一類普通通道,我們可以在通道服務(wù)器集群和工作進(jìn)程中無差別的做負(fù)載均衡,任何工作進(jìn)程都可以通用的處理這些消息。但響應(yīng)消息只能發(fā)送到通道服務(wù)器群中、正在監(jiān)聽該響應(yīng)的通道服務(wù)器上。

因此,Channels將其視為兩種不同的通道類型,在響應(yīng)通道名稱中包含感嘆號!來標(biāo)識,例如http.response!f5g3fe21f。普通通道名稱不包含感嘆號。此外,通道名稱都只能包含字符a-z A-Z 0-9 - _,并且長度小于200個字符。

對于后端實(shí)現(xiàn)來說,不一定要處理這個區(qū)別——只有擴(kuò)大服務(wù)器集群規(guī)模時,才有必要分別處理這兩類通道。有關(guān)規(guī)?;母嘈畔ⅲ约熬帉懞蠖嘶蚪涌诜?wù)器時如何處理通道類型,請參見:規(guī)?;?/a>。

分組

通道只能向一個監(jiān)聽方發(fā)送消息,不能廣播。如果要向一批客戶端發(fā)送消息,你需要記錄所有客戶端對應(yīng)的響應(yīng)通道。

如果我有一個Liveblog,想在發(fā)布新帖子時推送更新,我可以注冊一個程序處理post_save信號,同時得記下要發(fā)送更新的通道(這個例子使用Redis):

redis_conn = redis.Redis("localhost", 6379)

@receiver(post_save, sender=BlogUpdate)
def send_update(sender, instance, **kwargs):
    # 遍歷所有的reply channels,發(fā)送更新
    for reply_channel in redis_conn.smembers("readers"):
        Channel(reply_channel).send({
            "text": json.dumps({
                "id": instance.id,
                "content": instance.content
            })
        })

# 連接到websocket.connect
def ws_connect(message):
    # 添加到readers集合
    redis_conn.sadd("readers", message.reply_channel.name)

這段代碼可以運(yùn)行,但還有點(diǎn)小問題——客戶端中斷連接的時候,我們要記得把他們從readers中刪除。為此我們要添加另一個消費(fèi)者程序,監(jiān)聽和處理websocket.disconnect消息。而且,接口服務(wù)器可能遇到強(qiáng)退、斷電等情況,來不及發(fā)送disconnect信號,你的代碼永遠(yuǎn)收不到disconnect通知,但此時響應(yīng)通道已經(jīng)完全失效了,為此我們還需要添加過期機(jī)制,發(fā)送到響應(yīng)通道的消息等待一段時間后會過期、被刪除,

由于通道的設(shè)計(jì)基礎(chǔ)是無狀態(tài)的,所以如果接口服務(wù)器中斷連接,通道服務(wù)器也無所謂“關(guān)閉”一條通道——通道的任務(wù)就是保存消息直到消費(fèi)者出現(xiàn)(對某些類型的接口服務(wù)器比如SMS網(wǎng)關(guān),理論上可以為來自任何接口服務(wù)器的任何客戶端提供服務(wù))。

我們不太介意斷開連接的客戶端是不是沒收到消息——是它自己斷開的——但持續(xù)維護(hù)這些斷開的客戶端會讓通道后端很亂,累積造成通道名稱重復(fù)、發(fā)錯消息,這讓我們很介意。

現(xiàn)在,我們回到前面的示例,需要添加過期集合、跟蹤過期時間等等,但使用框架的意義就是替你實(shí)現(xiàn)這些重復(fù)的工作。于是Channels將此抽象實(shí)現(xiàn)為一個核心概念,叫做

@receiver(post_save, sender=BlogUpdate)
def send_update(sender, instance, **kwargs):
    Group("liveblog").send({
        "text": json.dumps({
            "id": instance.id,
            "content": instance.content
        })
    })

# 連接到websocket.connect
def ws_connect(message):
    # 添加到readers組
    Group("liveblog").add(message.reply_channel)
    # Accept the connection request
    message.reply_channel.send({"accept": True})

# 連接到websocket.disconnect
def ws_disconnect(message):
    # Remove from reader group on clean disconnect
    Group("liveblog").discard(message.reply_channel)

組不僅有自己的send()方法(后端可以提供高效的實(shí)現(xiàn)),而且還自動管理組成員過期——當(dāng)通道消息沒人讀而開始過期時,我們找到所有包含該通道的組,將其從中刪除。當(dāng)然,如果能正常收到disconnect消息的話,你還是應(yīng)該在斷開連接時將渠道從組中刪除,過期機(jī)制是為了解決無法正常收到disconnect消息的情況。

組通常只用于響應(yīng)通道(包含感嘆號!的通道),但如果你愿意,也可以用于普通通道。

下一步

這是對通道和組的高級概述,幫助你形成一些初步概念。要記住,Django提供了一些通道,但您可以自由地創(chuàng)建和使用自己的通道,所有通道都是網(wǎng)絡(luò)透明的。

另外,通道不保證消息發(fā)送成功。如果你需要確保任務(wù)完成,請使用專門設(shè)計(jì)的、帶有重試和持久化功能的系統(tǒng)(例如Celery)。你也可以創(chuàng)建管理命令,如果有任務(wù)沒完成,就再次向通道提交消息(換言之,自己維護(hù)重試邏輯)。

我們將在其余文檔中更多地介紹什么任務(wù)適合用通道,現(xiàn)在讓我們進(jìn)到開始來寫些代碼。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn),斷路器,智...
    卡卡羅2017閱讀 136,525評論 19 139
  • 國家電網(wǎng)公司企業(yè)標(biāo)準(zhǔn)(Q/GDW)- 面向?qū)ο蟮挠秒娦畔?shù)據(jù)交換協(xié)議 - 報批稿:20170802 前言: 排版 ...
    庭說閱讀 12,321評論 6 13
  • 從三月份找實(shí)習(xí)到現(xiàn)在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發(fā)崗...
    時芥藍(lán)閱讀 42,774評論 11 349
  • 一、概念(載錄于:http://www.cnblogs.com/EricaMIN1987_IT/p/3837436...
    yuantao123434閱讀 8,726評論 6 152
  • 如果,您還可以看一眼現(xiàn)在的我, 您一定不會失望, 您一定會夸我做的很好。 我沒有讓您失望, 沒有忘記你臨終前對我說...
    言諾生生閱讀 222評論 0 0

友情鏈接更多精彩內(nèi)容