學(xué)習(xí)過一定的web知識的都知道我們與服務(wù)器的交互并不是實時的,只有我們找服務(wù)器發(fā)送請求它才會返回一個響應(yīng),這樣子就很難做到一個實時性的要求,而如果服務(wù)器要主動向用戶發(fā)送數(shù)據(jù),那就要使用到websocket功能。常見的服務(wù)器主動向用戶發(fā)送數(shù)據(jù)的例子是在線聊天室,接下來我們就介紹一下一個Django使用channels和websocket大招在線聊天室的案例,來學(xué)習(xí)一下怎么使用websocket吧。
Channels是Django團隊研發(fā)的一個給Django提供websocket支持的框架,它同時支持http和websocket多種協(xié)議。使用channels可以讓你的Django應(yīng)用擁有實時通訊和給用戶主動推送信息的功能。
演示效果如下所示:
什么是websocket?
WebSocket 是 HTML5 開始提供的一種在單個 TCP 連接上進行全雙工通訊的協(xié)議。WebSocket 使得客戶端和服務(wù)器之間的數(shù)據(jù)交換變得更加簡單,允許服務(wù)端主動向客戶端推送數(shù)據(jù)。在 WebSocket API 中,瀏覽器和服務(wù)器只需要完成一次握手,兩者之間就直接可以創(chuàng)建持久性的連接,并進行雙向數(shù)據(jù)傳輸。
很多網(wǎng)站為了實現(xiàn)推送技術(shù),所用的技術(shù)都是 Ajax 輪詢。輪詢是在特定的的時間間隔(如每1秒),由瀏覽器對服務(wù)器發(fā)出HTTP請求,然后由服務(wù)器返回最新的數(shù)據(jù)給客戶端的瀏覽器。這種傳統(tǒng)的模式帶來很明顯的缺點,即瀏覽器需要不斷的向服務(wù)器發(fā)出請求,然而HTTP請求可能包含較長的頭部,其中真正有效的數(shù)據(jù)可能只是很小的一部分,顯然這樣會浪費很多的帶寬等資源。Websocket能更好的節(jié)省服務(wù)器資源和帶寬,并且能夠更實時地進行通訊,早已成為一種非常流行必須掌握的技術(shù)。
第一步 準備工作
首先在虛擬環(huán)境中安裝django和channels(本項目使用了最新版本,均為3.X版本), 新建一個名為myproject的項目,新建一個app名為chat。如果windows下安裝報錯,如何解決自己網(wǎng)上去找吧。
pip install django==3.2.3
pip install channels==3.0.3
修改settings.py, 將channels和chat加入到INSTALLED_APPS里,并添加相應(yīng)配置,如下所示:
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels', # channels應(yīng)用
'chat',
]
# 設(shè)置ASGI應(yīng)用
ASGI_APPLICATION = 'myproject.asgi.application'
# 設(shè)置通道層的通信后臺 - 本地測試用
CHANNEL_LAYERS = {
"default": {
"BACKEND": "channels.layers.InMemoryChannelLayer"
}
}
注意 :本例為了簡化代碼,使用了InMemoryChannelLayer做通道層(channel_layer)的通信后臺,實際生產(chǎn)環(huán)境中應(yīng)該需要使用redis作為后臺。這時你還需要安裝redis和channels_redis,然后添加如下配置:
# 生產(chǎn)環(huán)境中使用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應(yīng)用的urls.py加入到項目urls.py中去,這和常規(guī)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普通視圖函數(shù)編寫兩個頁面,一個用于展示首頁(index), 通過表單讓用戶輸入聊天室的名稱(room_name),然后跳轉(zhuǎn)到相應(yīng)聊天室頁面;一個頁面用于實時展示聊天信息記錄,并允許用戶發(fā)送信息。
這兩個頁面對應(yīng)的路由及視圖函數(shù)如下所示:
# 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內(nèi)容如下所示。它也基本不涉及websocket,就是讓用戶輸入聊天室后進行跳轉(zhuǎn)。
<!-- 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內(nèi)容如下所示。為了幫助你理解前后端是怎么實現(xiàn)websocket實時通信的,我給每行js代碼添加了注釋,這對于你理解前端如何發(fā)送websocket的請求,如果處理后端發(fā)過來的websocket消息至關(guān)重要。
<script>
// 獲取房間名
const roomName = JSON.parse(document.getElementById('room-name').textContent);
// 根據(jù)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連接時觸發(fā)此方法,展示歡迎提示
chatSocket.onopen = function(e) {
document.querySelector('#chat-log').value += ('[公告]歡迎來到' + roomName + '討論群。請文明發(fā)言!
')
}
// 從后臺接收到數(shù)據(jù)時觸發(fā)此方法
// 接收到后臺數(shù)據(jù)后對其解析,并加入到聊天記錄chat-log
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
document.querySelector('#chat-log').value += (data.message + '
');
};
// websocket連接斷開時觸發(fā)此方法
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();
}
};
// 每當(dāng)點擊發(fā)送消息按鈕,通過websocket的send方法向后臺發(fā)送信息。
document.querySelector('#chat-message-submit').onclick = function(e) {
const messageInputDom = document.querySelector('#chat-message-input');
const message = messageInputDom.value;
//注意這里:先把文本數(shù)據(jù)轉(zhuǎn)成json格式,然后調(diào)用send方法發(fā)送。
chatSocket.send(JSON.stringify({
'message': message
}));
messageInputDom.value = '';
};
</script>
此時如果你使用python manage.py runserver命令啟動測試服務(wù)器,當(dāng)你訪問一個名為/hello/的房間時,你將看到如下頁面:
到這里你看不到任何聊天記錄,也不能發(fā)送任何消息,因為我們還沒有在后端編寫任何代碼用于處理前端發(fā)來的消息,并返回數(shù)據(jù)。在終端你還會看到如下報錯, 說Django只能處理http連接,不能處理websocket。
到目前為止,我們所寫的就是一個普通的django應(yīng)用,還沒有用到channels庫處理websocket請求。接下來我們就要正式開始使用channels了。
第三步 編寫后臺websocket路由及處理方法
當(dāng) Django 接受 HTTP 請求時, 它會根據(jù)根 URLconf 以查找視圖函數(shù), 然后調(diào)用視圖函數(shù)來處理請求。同樣, 當(dāng) channels 接受 WebSocket 連接時, 它也會根據(jù)根路由配置去查找相應(yīng)的處理方法。只不過channels的路由不在urls.py中配置,處理方法也不寫在views.py。在channels中,這兩個文件分別變成了routing.py和consumers.py。這樣的好處是不用和django的常規(guī)應(yīng)用混在一起。
- routing.py:websocket路由文件,相當(dāng)于django的urls.py。它根據(jù)websocket請求的url地址觸發(fā)consumers.py里定義的方法。
- consumers.py:相當(dāng)于django的視圖views.py,負責(zé)處理通過websocket路由轉(zhuǎn)發(fā)過來的請求和數(shù)據(jù)。
在chat應(yīng)用下新建routing.py, 添加如下代碼。它的作用是將發(fā)送至ws/chat/<room_name>/的websocket請求轉(zhuǎn)由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) 來區(qū)分 WebSocket 連接與普通 HTTP 連接, 因為它將使生產(chǎn)環(huán)境中部署 Channels 更容易,比如nginx把所有/ws的請求轉(zhuǎn)給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會根據(jù)請求協(xié)議的類型來轉(zhuǎn)發(fā)請求。AuthMiddlewareStack將使用對當(dāng)前經(jīng)過身份驗證的用戶的引用來填充連接的scope, 類似于 Django 的request對象,我們后面還會講到。
接下來在chat應(yīng)用下新建consumers.py, 添加如下代碼:
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
import datetime
class ChatConsumer(WebsocketConsumer):
# websocket建立連接時執(zhí)行方法
def connect(self):
# 從url里獲取聊天室名字,為每個房間建立一個頻道組
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name
# 將當(dāng)前頻道加入頻道組
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
# 接受所有websocket請求
self.accept()
# websocket斷開時執(zhí)行方法
def disconnect(self, close_code):
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
# 從websocket接收到消息時執(zhí)行函數(shù)
def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
# 發(fā)送消息到頻道組,頻道組調(diào)用chat_message方法
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
# 從頻道組接收到消息后執(zhí)行方法
def chat_message(self, event):
message = event['message']
datetime_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# 通過websocket發(fā)送消息到客戶端
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種方法, 可以給單個頻道或一個頻道組發(fā)信息,還可以將一個頻道加入到組。
每個頻道(channel)都有一個名字。擁有頻道名稱的任何人都可以向頻道發(fā)送消息。
一個組(group)有一個名字。具有組名稱的任何人都可以按名稱向組添加/刪除頻道,并向組中的所有頻道發(fā)送消息。
注意:雖然異步Consumer類性能更優(yōu),channels推薦使用同步consumer類 , 尤其是調(diào)用Django ORM或其他同步程序時,以保持整個consumer在單個線程中并避免ORM查詢阻塞整個event。調(diào)用channel_layer提供的方法時需要用async_to_sync轉(zhuǎn)換一下。
除此以外,我們還使用了self.scope['url_route']['kwargs']['room_name']從路由中獲取了聊天室的房間名,在channels程序中,scope是個很重要的對象,類似于django的request對象,它代表了當(dāng)前websocket連接的所有信息。你可以通過scope['user']獲取當(dāng)前用戶對象,還可以通過scope['path']獲取當(dāng)前當(dāng)前請求路徑。
第四步 運行看效果
如果不出意外,你現(xiàn)在的項目布局應(yīng)該如下所示:
連續(xù)運行如下命令,就可以看到我們文初的效果啦。
python manage.py makemigrations
python manage.py migrate
python manage.py runserver
小結(jié)
我們已經(jīng)使用django + channels 寫了個在線聊天小應(yīng)用了,現(xiàn)在來總結(jié)下我們所學(xué)的知識吧。
- websocket屬于全雙工通訊的協(xié)議,可以在服務(wù)器和客戶端之間保持長連接,實現(xiàn)雙向數(shù)據(jù)傳輸。
- 前端創(chuàng)建websocket對象后可以通過onmessage監(jiān)聽并處理后端返回的數(shù)據(jù),可以通過send方法向后端發(fā)送數(shù)據(jù)。
- channels對應(yīng)websocket的路由和處理方法分別寫在routing.py和consumers.py文件里,相當(dāng)于django的urls.py和views.py。
- 每個頻道(channel)都有一個名字,擁有頻道名稱的任何人都可以向頻道發(fā)送消息。一個組(group)有一個名字,可以包含多個頻道。
- 每個自定義的Consumer類自帶 self.channel_name 和self.channel_layer 屬性。前者是獨一無二的頻道名,后者提供了 send(), group_send()和group_add() 3種方法。
- 在channels程序中,scope是個很重要的對象,類似于django的request對象,它代表了當(dāng)前websocket連接的所有信息,比如scope['user'], scope['path']。
以上就是Django使用channels和websocket打造在線聊天室的詳細內(nèi)容,更多Django和websocket的學(xué)習(xí)資料請關(guān)注W3Cschool其它相關(guān)文章!