Django使用Channels實(shí)現(xiàn)WebSocket的方法
WebSocket - 開啟通往新世界的大門
WebSocket是什么?
WebSocket是一種在單個(gè)TCP連接上進(jìn)行全雙工通訊的協(xié)議。WebSocket允許服務(wù)端主動(dòng)向客戶端推送數(shù)據(jù)。在WebSocket協(xié)議中,客戶端瀏覽器和服務(wù)器只需要完成一次握手就可以創(chuàng)建持久性的連接,并在瀏覽器和服務(wù)器之間進(jìn)行雙向的數(shù)據(jù)傳輸。
WebSocket有什么用?
WebSocket區(qū)別于HTTP協(xié)議的一個(gè)最為顯著的特點(diǎn)是,WebSocket協(xié)議可以由服務(wù)端主動(dòng)發(fā)起消息,對(duì)于瀏覽器需要及時(shí)接收數(shù)據(jù)變化的場(chǎng)景非常適合,例如在Django中遇到一些耗時(shí)較長(zhǎng)的任務(wù)我們通常會(huì)使用Celery來(lái)異步執(zhí)行,那么瀏覽器如果想要獲取這個(gè)任務(wù)的執(zhí)行狀態(tài),在HTTP協(xié)議中只能通過(guò)輪訓(xùn)的方式由瀏覽器不斷的發(fā)送請(qǐng)求給服務(wù)器來(lái)獲取最新狀態(tài),這樣發(fā)送很多無(wú)用的請(qǐng)求不僅浪費(fèi)資源,還不夠優(yōu)雅,如果使用WebSokcet來(lái)實(shí)現(xiàn)就很完美了
WebSocket的另外一個(gè)應(yīng)用場(chǎng)景就是下文要說(shuō)的聊天室,一個(gè)用戶(瀏覽器)發(fā)送的消息需要實(shí)時(shí)的讓其他用戶(瀏覽器)接收,這在HTTP協(xié)議下是很難實(shí)現(xiàn)的,但WebSocket基于長(zhǎng)連接加上可以主動(dòng)給瀏覽器發(fā)消息的特性處理起來(lái)就游刃有余了
初步了解WebSocket之后,我們看看如何在Django中實(shí)現(xiàn)WebSocket
Channels
Django本身不支持WebSocket,但可以通過(guò)集成Channels框架來(lái)實(shí)現(xiàn)WebSocket
Channels是針對(duì)Django項(xiàng)目的一個(gè)增強(qiáng)框架,可以使Django不僅支持HTTP協(xié)議,還能支持WebSocket,MQTT等多種協(xié)議,同時(shí)Channels還整合了Django的auth以及session系統(tǒng)方便進(jìn)行用戶管理及認(rèn)證。
我下文所有的代碼實(shí)現(xiàn)使用以下python和Django版本
python==3.6.3 django==2.2
集成Channels
我假設(shè)你已經(jīng)新建了一個(gè)django項(xiàng)目,項(xiàng)目名字就叫webapp,目錄結(jié)構(gòu)如下
project - webapp - __init__.py - settings.py - urls.py - wsgi.py - manage.py
1. 安裝channels
pip install channels==2.1.7
2. 修改settings.py文件,
# APPS中添加channels INSTALLED_APPS = [ 'django.contrib.staticfiles', 'channels', ] # 指定ASGI的路由地址 ASGI_APPLICATION = 'webapp.routing.application'
channels運(yùn)行于ASGI協(xié)議上,ASGI的全名是Asynchronous Server Gateway Interface。它是區(qū)別于Django使用的WSGI協(xié)議 的一種異步服務(wù)網(wǎng)關(guān)接口協(xié)議,正是因?yàn)樗艑?shí)現(xiàn)了websocket
ASGI_APPLICATION 指定主路由的位置為webapp下的routing.py文件中的application
3. setting.py的同級(jí)目錄下創(chuàng)建routing.py路由文件,routing.py類似于Django中的url.py指明websocket協(xié)議的路由
from channels.routing import ProtocolTypeRouter application = ProtocolTypeRouter({ # 暫時(shí)為空,下文填充 })
4. 運(yùn)行Django項(xiàng)目
C:\python36\python.exe D:/demo/tailf/manage.py runserver 0.0.0.0:80 Performing system checks... Watching for file changes with StatReloader System check identified no issues (0 silenced). April 12, 2019 - 17:44:52 Django version 2.2, using settings 'webapp.settings' Starting ASGI/Channels version 2.1.7 development server at http://0.0.0.0:80/ Quit the server with CTRL-BREAK.
仔細(xì)觀察上邊的輸出會(huì)發(fā)現(xiàn)Django啟動(dòng)中的Starting development server已經(jīng)變成了Starting ASGI/Channels version 2.1.7 development server,這表明項(xiàng)目已經(jīng)由django使用的WSGI協(xié)議轉(zhuǎn)換為了Channels使用的ASGI協(xié)議
至此Django已經(jīng)基本集成了Channels框架
構(gòu)建聊天室
上邊雖然在項(xiàng)目中集成了Channels,但并沒有任何的應(yīng)用使用它,接下來(lái)我們以聊天室的例子來(lái)講解Channels的使用
假設(shè)你已經(jīng)創(chuàng)建好了一個(gè)叫chat的app,并添加到了settings.py的INSTALLED_APPS中,app的目錄結(jié)構(gòu)大概如下
chat - migrations - __init__.py - __init__.py - admin.py - apps.py - models.py - tests.py - views.py
我們構(gòu)建一個(gè)標(biāo)準(zhǔn)的Django聊天頁(yè)面,相關(guān)代碼如下
url: from django.urls import path from chat.views import chat urlpatterns = [ path('chat', chat, name='chat-url') ] view: from django.shortcuts import render def chat(request): return render(request, 'chat/index.html') template: {% extends "base.html" %} {% block content %} <textarea class="form-control" id="chat-log" disabled rows="20"></textarea><br/> <input class="form-control" id="chat-message-input" type="text"/><br/> <input class="btn btn-success btn-block" id="chat-message-submit" type="button" value="Send"/> {% endblock %}
通過(guò)上邊的代碼一個(gè)簡(jiǎn)單的web聊天頁(yè)面構(gòu)建完成了,訪問頁(yè)面大概樣子如下:
接下來(lái)我們利用Channels的WebSocket協(xié)議實(shí)現(xiàn)消息的發(fā)送接收功能
1. 先從路由入手,上邊我們已經(jīng)創(chuàng)建了routing.py路由文件,現(xiàn)在來(lái)填充里邊的內(nèi)容
from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter import chat.routing application = ProtocolTypeRouter({ 'websocket': AuthMiddlewareStack( URLRouter( chat.routing.websocket_urlpatterns ) ), })
ProtocolTypeRouter: ASIG支持多種不同的協(xié)議,在這里可以指定特定協(xié)議的路由信息,我們只使用了websocket協(xié)議,這里只配置websocket即可
AuthMiddlewareStack: django的channels封裝了django的auth模塊,使用這個(gè)配置我們就可以在consumer中通過(guò)下邊的代碼獲取到用戶的信息
def connect(self): self.user = self.scope["user"]
self.scope類似于django中的request,包含了請(qǐng)求的type、path、header、cookie、session、user等等有用的信息
URLRouter: 指定路由文件的路徑,也可以直接將路由信息寫在這里,代碼中配置了路由文件的路徑,會(huì)去chat下的routeing.py文件中查找websocket_urlpatterns,chat/routing.py內(nèi)容如下
from django.urls import path from chat.consumers import ChatConsumer websocket_urlpatterns = [ path('ws/chat/', ChatConsumer), ]
routing.py路由文件跟django的url.py功能類似,語(yǔ)法也一樣,意思就是訪問ws/chat/都交給ChatConsumer處理
2. 接著編寫consumer,consumer類似django中的view,內(nèi)容如下
from channels.generic.websocket import WebsocketConsumer import json class ChatConsumer(WebsocketConsumer): def connect(self): self.accept() def disconnect(self, close_code): pass def receive(self, text_data): text_data_json = json.loads(text_data) message = '運(yùn)維咖啡吧:' + text_data_json['message'] self.send(text_data=json.dumps({ 'message': message }))
這里是個(gè)最簡(jiǎn)單的同步websocket consumer類,connect方法在連接建立時(shí)觸發(fā),disconnect在連接關(guān)閉時(shí)觸發(fā),receive方法會(huì)在收到消息后觸發(fā)。整個(gè)ChatConsumer類會(huì)將所有收到的消息加上“運(yùn)維咖啡吧:”的前綴發(fā)送給客戶端
3. 最后我們?cè)趆tml模板頁(yè)面添加websocket支持
{% extends "base.html" %} {% block content %} <textarea class="form-control" id="chat-log" disabled rows="20"></textarea><br/> <input class="form-control" id="chat-message-input" type="text"/><br/> <input class="btn btn-success btn-block" id="chat-message-submit" type="button" value="Send"/> {% endblock %} {% block js %} <script> var chatSocket = new WebSocket( 'ws://' + window.location.host + '/ws/chat/'); chatSocket.onmessage = function(e) { var data = JSON.parse(e.data); var message = data['message']; document.querySelector('#chat-log').value += (message + '\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': message })); messageInputDom.value = ''; }; </script> {% endblock %}
WebSocket對(duì)象一個(gè)支持四個(gè)消息:onopen,onmessage,oncluse和onerror,我們這里用了兩個(gè)onmessage和onclose
onopen: 當(dāng)瀏覽器和websocket服務(wù)端連接成功后會(huì)觸發(fā)onopen消息
onerror: 如果連接失敗,或者發(fā)送、接收數(shù)據(jù)失敗,或者數(shù)據(jù)處理出錯(cuò)都會(huì)觸發(fā)onerror消息
onmessage: 當(dāng)瀏覽器接收到websocket服務(wù)器發(fā)送過(guò)來(lái)的數(shù)據(jù)時(shí),就會(huì)觸發(fā)onmessage消息,參數(shù)e包含了服務(wù)端發(fā)送過(guò)來(lái)的數(shù)據(jù)
onclose: 當(dāng)瀏覽器接收到websocket服務(wù)器發(fā)送過(guò)來(lái)的關(guān)閉連接請(qǐng)求時(shí),會(huì)觸發(fā)onclose消息
4. 完成前邊的代碼,一個(gè)可以聊天的websocket頁(yè)面就完成了,運(yùn)行項(xiàng)目,在瀏覽器中輸入消息就會(huì)通過(guò)websocket-->rouging.py-->consumer.py處理后返回給前端
啟用Channel Layer
上邊的例子我們已經(jīng)實(shí)現(xiàn)了消息的發(fā)送和接收,但既然是聊天室,肯定要支持多人同時(shí)聊天的,當(dāng)我們打開多個(gè)瀏覽器分別輸入消息后發(fā)現(xiàn)只有自己收到消息,其他瀏覽器端收不到,如何解決這個(gè)問題,讓所有客戶端都能一起聊天呢?
Channels引入了一個(gè)layer的概念,channel layer是一種通信系統(tǒng),允許多個(gè)consumer實(shí)例之間互相通信,以及與外部Djanbo程序?qū)崿F(xiàn)互通。
channel layer主要實(shí)現(xiàn)了兩種概念抽象:
channel name: channel實(shí)際上就是一個(gè)發(fā)送消息的通道,每個(gè)Channel都有一個(gè)名稱,每一個(gè)擁有這個(gè)名稱的人都可以往Channel里邊發(fā)送消息
group: 多個(gè)channel可以組成一個(gè)Group,每個(gè)Group都有一個(gè)名稱,每一個(gè)擁有這個(gè)名稱的人都可以往Group里添加/刪除Channel,也可以往Group里發(fā)送消息,Group內(nèi)的所有channel都可以收到,但是無(wú)法發(fā)送給Group內(nèi)的具體某個(gè)Channel
了解了上邊的概念,接下來(lái)我們利用channel layer實(shí)現(xiàn)真正的聊天室,能夠讓多個(gè)客戶端發(fā)送的消息被彼此看到
1. 官方推薦使用redis作為channel layer,所以先安裝channels_redis
pip install channels_redis==2.3.3
2. 然后修改settings.py添加對(duì)layer的支持
CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('ops-coffee.cn', 6379)], }, }, }
添加channel之后我們可以通過(guò)以下命令檢查通道層是否能夠正常工作
>python manage.py shell Python 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> import channels.layers >>> channel_layer = channels.layers.get_channel_layer() >>> >>> from asgiref.sync import async_to_sync >>> async_to_sync(channel_layer.send)('test_channel',{'site':'https://ops-coffee.cn'}) >>> async_to_sync(channel_layer.receive)('test_channel') {'site': 'https://ops-coffee.cn'} >>>
3. consumer做如下修改引入channel layer
from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer import json class ChatConsumer(WebsocketConsumer): def connect(self): self.room_group_name = 'ops_coffee' # Join room group async_to_sync(self.channel_layer.group_add)( self.room_group_name, self.channel_name ) self.accept() def disconnect(self, close_code): # Leave room group async_to_sync(self.channel_layer.group_discard)( self.room_group_name, self.channel_name ) # Receive message from WebSocket def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json['message'] # Send message to room group async_to_sync(self.channel_layer.group_send)( self.room_group_name, { 'type': 'chat_message', 'message': message } ) # Receive message from room group def chat_message(self, event): message = '運(yùn)維咖啡吧:' + event['message'] # Send message to WebSocket self.send(text_data=json.dumps({ 'message': message }))
這里我們?cè)O(shè)置了一個(gè)固定的房間名作為Group name,所有的消息都會(huì)發(fā)送到這個(gè)Group里邊,當(dāng)然你也可以通過(guò)參數(shù)的方式將房間名傳進(jìn)來(lái)作為Group name,從而建立多個(gè)Group,這樣可以實(shí)現(xiàn)僅同房間內(nèi)的消息互通
當(dāng)我們啟用了channel layer之后,所有與consumer之間的通信將會(huì)變成異步的,所以必須使用async_to_sync
一個(gè)鏈接(channel)創(chuàng)建時(shí),通過(guò)group_add將channel添加到Group中,鏈接關(guān)閉通過(guò)group_discard將channel從Group中剔除,收到消息時(shí)可以調(diào)用group_send方法將消息發(fā)送到Group,這個(gè)Group內(nèi)所有的channel都可以收的到
group_send中的type指定了消息處理的函數(shù),這里會(huì)將消息轉(zhuǎn)給chat_message函數(shù)去處理
4. 經(jīng)過(guò)以上的修改,我們?cè)俅卧诙鄠€(gè)瀏覽器上打開聊天頁(yè)面輸入消息,發(fā)現(xiàn)彼此已經(jīng)能夠看到了,至此一個(gè)完整的聊天室已經(jīng)基本完成
修改為異步
我們前邊實(shí)現(xiàn)的consumer是同步的,為了能有更好的性能,官方支持異步的寫法,只需要修改consumer.py即可
from channels.generic.websocket import AsyncWebsocketConsumer import json class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): self.room_group_name = 'ops_coffee' # Join room group await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() async def disconnect(self, close_code): # Leave room group await self.channel_layer.group_discard( self.room_group_name, self.channel_name ) # Receive message from WebSocket async def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json['message'] # Send message to room group await self.channel_layer.group_send( self.room_group_name, { 'type': 'chat_message', 'message': message } ) # Receive message from room group async def chat_message(self, event): message = '運(yùn)維咖啡吧:' + event['message'] # Send message to WebSocket await self.send(text_data=json.dumps({ 'message': message }))
其實(shí)異步的代碼跟之前的差別不大,只有幾個(gè)小區(qū)別:
ChatConsumer由WebsocketConsumer修改為了AsyncWebsocketConsumer
所有的方法都修改為了異步defasync def
用await來(lái)實(shí)現(xiàn)異步I/O的調(diào)用
channel layer也不再需要使用async_to_sync了
好了,現(xiàn)在一個(gè)完全異步且功能完整的聊天室已經(jīng)構(gòu)建完成了
代碼地址
我已經(jīng)將以上的演示代碼上傳至Github方便你在實(shí)現(xiàn)的過(guò)程中查看參考,具體地址為:
https://github.com/ops-coffee/demo/tree/master/websocket
總結(jié)
以上所述是小編給大家介紹的Django使用Channels實(shí)現(xiàn)WebSocket的方法,希望對(duì)大家有所幫助,如果大家有任何疑問請(qǐng)給我留言,小編會(huì)及時(shí)回復(fù)大家的。在此也非常感謝大家對(duì)腳本之家網(wǎng)站的支持!
如果你覺得本文對(duì)你有幫助,歡迎轉(zhuǎn)載,煩請(qǐng)注明出處,謝謝!
- Django實(shí)現(xiàn)WebSocket在線聊天室功能(channels庫(kù))
- Django中如何使用Channels功能
- Django使用channels + websocket打造在線聊天室
- django使用channels實(shí)現(xiàn)通信的示例
- 淺談django channels 路由誤導(dǎo)
- 詳解Django-channels 實(shí)現(xiàn)WebSocket實(shí)例
- Django Channels 實(shí)現(xiàn)點(diǎn)對(duì)點(diǎn)實(shí)時(shí)聊天和消息推送功能
- django使用channels2.x實(shí)現(xiàn)實(shí)時(shí)通訊
- django channels使用和配置及實(shí)現(xiàn)群聊
相關(guān)文章
詳解python os.path.exists判斷文件或文件夾是否存在
這篇文章主要介紹了詳解python os.path.exists判斷文件或文件夾是否存在,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2020-11-11python numpy中mat和matrix的區(qū)別
這篇文章主要介紹了python numpy中mat和matrix的區(qū)別,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2021-03-03基于Python實(shí)現(xiàn)一個(gè)自動(dòng)關(guān)機(jī)程序并打包成exe文件
這篇文章主要介紹了通過(guò)Python創(chuàng)建一個(gè)可以自動(dòng)關(guān)機(jī)的小程序,并打包成exe文件。文中的示例代碼講解詳細(xì),對(duì)我們學(xué)習(xí)Python有一定的幫助,感興趣的同學(xué)可以了解一下2021-12-12Python中的函數(shù)參數(shù)(位置參數(shù)、默認(rèn)參數(shù)、可變參數(shù))
這篇文章主要介紹了Python中的函數(shù)參數(shù):位置參數(shù)、默認(rèn)參數(shù)、可變參數(shù)、關(guān)鍵字參數(shù)和命名關(guān)鍵字參數(shù),需要的小伙伴可以參考下面文章內(nèi)容2021-09-09用pip給python安裝matplotlib庫(kù)的詳細(xì)教程
這篇文章主要介紹了用pip給python安裝matplotlib庫(kù)的詳細(xì)教程,本文通過(guò)圖文并茂的形式給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2021-02-02python自動(dòng)化發(fā)送郵件實(shí)例講解
在本篇文章里小編給大家分享了一篇關(guān)于python自動(dòng)化發(fā)送郵件實(shí)例講解內(nèi)容,有興趣的朋友們可以學(xué)習(xí)參考下。2021-01-01tensorflow學(xué)習(xí)教程之文本分類詳析
初學(xué)tensorflow,借鑒了很多別人的經(jīng)驗(yàn),參考博客對(duì)評(píng)論分類(感謝博主的一系列好文),本人也嘗試著實(shí)現(xiàn)了對(duì)文本數(shù)據(jù)的分類,下面這篇文章主要給大家介紹了關(guān)于tensorflow學(xué)習(xí)教程之文本分類的相關(guān)資料,需要的朋友可以參考下2018-08-08