Django使用channels + websocket打造在線聊天室
Channels是Django團隊研發的一個給Django提供websocket支持的框架,它同時支持http和websocket多種協議。使用channels可以讓你的Django應用擁有實時通訊和給用戶主動推送信息的功能。
演示效果如下所示:
什麼是websocket?
WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協議。WebSocket 使得客戶端和服務器之間的數據交換變得更加簡單,允許服務端主動向客戶端推送數據。在 WebSocket API 中,瀏覽器和服務器隻需要完成一次握手,兩者之間就直接可以創建持久性的連接,並進行雙向數據傳輸。
很多網站為瞭實現推送技術,所用的技術都是 Ajax 輪詢。輪詢是在特定的的時間間隔(如每1秒),由瀏覽器對服務器發出HTTP請求,然後由服務器返回最新的數據給客戶端的瀏覽器。這種傳統的模式帶來很明顯的缺點,即瀏覽器需要不斷的向服務器發出請求,然而HTTP請求可能包含較長的頭部,其中真正有效的數據可能隻是很小的一部分,顯然這樣會浪費很多的帶寬等資源。Websocket能更好的節省服務器資源和帶寬,並且能夠更實時地進行通訊,早已成為一種非常流行必須掌握的技術。
第一步 準備工作
首先在虛擬環境中安裝django和channels(本項目使用瞭最新版本,均為3.X版本), 新建一個名為myproject的項目,新建一個app名為chat。如果windows下安裝報錯,如何解決自己網上去找吧。
pip install django==3.2.3
pip install channels==3.0.3
修改settings.py, 將channels和chat加入到INSTALLED_APPS裡,並添加相應配置,如下所示:
INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'channels', # channels應用 'chat', ] # 設置ASGI應用 ASGI_APPLICATION = 'myproject.asgi.application' # 設置通道層的通信後臺 - 本地測試用 CHANNEL_LAYERS = { "default": { "BACKEND": "channels.layers.InMemoryChannelLayer" } }
註意 :本例為瞭簡化代碼,使用瞭InMemoryChannelLayer做通道層(channel_layer)的通信後臺,實際生產環境中應該需要使用redis作為後臺。這時你還需要安裝redis和channels_redis,然後添加如下配置:
# 生產環境中使用redis做後臺,安裝channels_redis CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [("127.0.0.1", 6379)], #或"hosts": [os.environ.get('REDIS_URL', 'redis://127.0.0.1:6379/1')], }, }, }
最後將chat應用的urls.py加入到項目urls.py中去,這和常規Django項目無異。
# myproject/urls.py from django.conf.urls import include from django.urls import path from django.contrib import admin urlpatterns = [ path('chat/', include('chat.urls')), path('admin/', admin.site.urls), ]
第二步 編寫聊天室頁面
我們需要利用django普通視圖函數編寫兩個頁面,一個用於展示首頁(index), 通過表單讓用戶輸入聊天室的名稱(room_name),然後跳轉到相應聊天室頁面;一個頁面用於實時展示聊天信息記錄,並允許用戶發送信息。
這兩個頁面對應的路由及視圖函數如下所示:
# chat/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), path('<str:room_name>/', views.room, name='room'), ] # chat/views.py from django.shortcuts import render def index(request): return render(request, 'chat/index.html', {}) def room(request, room_name): return render(request, 'chat/room.html', { 'room_name': room_name })
接下來我們編寫兩個模板文件index.html和room.html。它們的路徑位置如下所示:
chat/ __init__.py templates/ chat/ index.html room.html urls.py views.py
index.html內容如下所示。它也基本不涉及websocket,就是讓用戶輸入聊天室後進行跳轉。
<!-- chat/templates/chat/index.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Chat Rooms</title> </head> <body> 請輸入聊天室名稱: <input id="room-name-input" type="text" size="100"> <input id="room-name-submit" type="button" value="Enter"> <script> document.querySelector('#room-name-input').focus(); document.querySelector('#room-name-input').onkeyup = function(e) { if (e.keyCode === 13) { // enter, return document.querySelector('#room-name-submit').click(); } }; document.querySelector('#room-name-submit').onclick = function(e) { var roomName = document.querySelector('#room-name-input').value; window.location.pathname = '/chat/' + roomName + '/'; }; </script> </body> </html>
room.html內容如下所示。為瞭幫助你理解前後端是怎麼實現websocket實時通信的,我給每行js代碼添加瞭註釋,這對於你理解前端如何發送websocket的請求,如果處理後端發過來的websocket消息至關重要。
<script> // 獲取房間名 const roomName = JSON.parse(document.getElementById('room-name').textContent); // 根據roomName拼接websocket請求地址,建立長連接 // 請求url地址為/ws/chat/<room_name>/ const wss_protocol = (window.location.protocol == 'https:') ? 'wss://': 'ws://'; const chatSocket = new WebSocket( wss_protocol + window.location.host + '/ws/chat/' + roomName + '/' ); // 建立websocket連接時觸發此方法,展示歡迎提示 chatSocket.onopen = function(e) { document.querySelector('#chat-log').value += ('[公告]歡迎來到' + roomName + '討論群。請文明發言!\n') } // 從後臺接收到數據時觸發此方法 // 接收到後臺數據後對其解析,並加入到聊天記錄chat-log chatSocket.onmessage = function(e) { const data = JSON.parse(e.data); document.querySelector('#chat-log').value += (data.message + '\n'); }; // websocket連接斷開時觸發此方法 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(); } }; // 每當點擊發送消息按鈕,通過websocket的send方法向後臺發送信息。 document.querySelector('#chat-message-submit').onclick = function(e) { const messageInputDom = document.querySelector('#chat-message-input'); const message = messageInputDom.value; //註意這裡:先把文本數據轉成json格式,然後調用send方法發送。 chatSocket.send(JSON.stringify({ 'message': message })); messageInputDom.value = ''; }; </script>
此時如果你使用python manage.py runserver命令啟動測試服務器,當你訪問一個名為/hello/的房間時,你將看到如下頁面:
到這裡你看不到任何聊天記錄,也不能發送任何消息,因為我們還沒有在後端編寫任何代碼用於處理前端發來的消息,並返回數據。在終端你還會看到如下報錯, 說Django隻能處理http連接,不能處理websocket。
到目前為止,我們所寫的就是一個普通的django應用,還沒有用到channels庫處理websocket請求。接下來我們就要正式開始使用channels瞭。
第三步 編寫後臺websocket路由及處理方法
當 Django 接受 HTTP 請求時, 它會根據根 URLconf 以查找視圖函數, 然後調用視圖函數來處理請求。同樣, 當 channels 接受 WebSocket 連接時, 它也會根據根路由配置去查找相應的處理方法。隻不過channels的路由不在urls.py中配置,處理方法也不寫在views.py。在channels中,這兩個文件分別變成瞭routing.py和consumers.py。這樣的好處是不用和django的常規應用混在一起。
- routing.py:websocket路由文件,相當於django的urls.py。它根據websocket請求的url地址觸發consumers.py裡定義的方法。
- consumers.py:相當於django的視圖views.py,負責處理通過websocket路由轉發過來的請求和數據。
在chat應用下新建routing.py, 添加如下代碼。它的作用是將發送至ws/chat/<room_name>/的websocket請求轉由ChatConsumer處理。
# chat/routing.py from django.urls import re_path from . import consumers websocket_urlpatterns = [ re_path(r'ws/chat/(?P<room_name>\w+)/$', consumers.ChatConsumer.as_asgi()), ]
註意:定義websocket路由時,推薦使用常見的路徑前綴 (如/ws) 來區分 WebSocket 連接與普通 HTTP 連接, 因為它將使生產環境中部署 Channels 更容易,比如nginx把所有/ws的請求轉給channels處理。
與Django類似,我們還需要把這個app的websocket路由加入到項目的根路由中去。編輯myproject/asgi.py, 添加如下代碼:
# myproject/asgi.py import os from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter from django.core.asgi import get_asgi_application import chat.routing os.environ.setdefault("DJANGO_SETTINGS_MODULE", "myproject.settings") application = ProtocolTypeRouter({ # http請求使用這個 "http": get_asgi_application(), # websocket請求使用這個 "websocket": AuthMiddlewareStack( URLRouter( chat.routing.websocket_urlpatterns ) ), })
在這裡,channels的ProtocolTypeRouter會根據請求協議的類型來轉發請求。AuthMiddlewareStack將使用對當前經過身份驗證的用戶的引用來填充連接的scope, 類似於 Django 的request對象,我們後面還會講到。
接下來在chat應用下新建consumers.py, 添加如下代碼:
import json from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer import datetime class ChatConsumer(WebsocketConsumer): # websocket建立連接時執行方法 def connect(self): # 從url裡獲取聊天室名字,為每個房間建立一個頻道組 self.room_name = self.scope['url_route']['kwargs']['room_name'] self.room_group_name = 'chat_%s' % self.room_name # 將當前頻道加入頻道組 async_to_sync(self.channel_layer.group_add)( self.room_group_name, self.channel_name ) # 接受所有websocket請求 self.accept() # websocket斷開時執行方法 def disconnect(self, close_code): async_to_sync(self.channel_layer.group_discard)( self.room_group_name, self.channel_name ) # 從websocket接收到消息時執行函數 def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json['message'] # 發送消息到頻道組,頻道組調用chat_message方法 async_to_sync(self.channel_layer.group_send)( self.room_group_name, { 'type': 'chat_message', 'message': message } ) # 從頻道組接收到消息後執行方法 def chat_message(self, event): message = event['message'] datetime_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') # 通過websocket發送消息到客戶端 self.send(text_data=json.dumps({ 'message': f'{datetime_str}:{message}' }))
每個自定義的Consumer類一般繼承同步的WebsocketConsumer類或異步的AysncWebSocketConsumer類,它自帶 self.channel_name 和self.channel_layer 屬性。前者是獨一無二的長連接頻道名,後者提供瞭 send(), group_send()和group_add() 3種方法, 可以給單個頻道或一個頻道組發信息,還可以將一個頻道加入到組。
每個頻道(channel)都有一個名字。擁有頻道名稱的任何人都可以向頻道發送消息。
一個組(group)有一個名字。具有組名稱的任何人都可以按名稱向組添加/刪除頻道,並向組中的所有頻道發送消息。
註意:雖然異步Consumer類性能更優,channels推薦使用同步consumer類 , 尤其是調用Django ORM或其他同步程序時,以保持整個consumer在單個線程中並避免ORM查詢阻塞整個event。調用channel_layer提供的方法時需要用async_to_sync轉換一下。
除此以外,我們還使用瞭self.scope[‘url_route’][‘kwargs’][‘room_name’]從路由中獲取瞭聊天室的房間名,在channels程序中,scope是個很重要的對象,類似於django的request對象,它代表瞭當前websocket連接的所有信息。你可以通過scope[‘user’]獲取當前用戶對象,還可以通過scope[‘path’]獲取當前當前請求路徑。
第四步 運行看效果
如果不出意外,你現在的項目佈局應該如下所示:
連續運行如下命令,就可以看到我們文初的效果啦。
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
小結
我們已經使用django + channels 寫瞭個在線聊天小應用瞭,現在來總結下我們所學的知識吧。
- websocket屬於全雙工通訊的協議,可以在服務器和客戶端之間保持長連接,實現雙向數據傳輸。
- 前端創建websocket對象後可以通過onmessage監聽並處理後端返回的數據,可以通過send方法向後端發送數據。
- channels對應websocket的路由和處理方法分別寫在routing.py和consumers.py文件裡,相當於django的urls.py和views.py。
- 每個頻道(channel)都有一個名字,擁有頻道名稱的任何人都可以向頻道發送消息。一個組(group)有一個名字,可以包含多個頻道。
- 每個自定義的Consumer類自帶 self.channel_name 和self.channel_layer 屬性。前者是獨一無二的頻道名,後者提供瞭 send(), group_send()和group_add() 3種方法。
- 在channels程序中,scope是個很重要的對象,類似於django的request對象,它代表瞭當前websocket連接的所有信息,比如scope[‘user’], scope[‘path’]。
本文的知識你學會瞭嗎? 學到瞭就點個贊吧!下期我們將利用channels + celery + redis打造個聊天機器人,歡迎關註!
以上就是Django使用channels + websocket打造在線聊天室的詳細內容,更多關於Django 在線聊天室的資料請關註WalkonNet其它相關文章!
推薦閱讀:
- django channels使用和配置及實現群聊
- nodejs結合Socket.IO實現websocket即時通訊
- Python利用Telegram機器人搭建消息提醒
- SpringBoot+WebSocket實現多人在線聊天案例實例
- Django項目創建的圖文教程