準(zhǔn)備工作和說明:
1.安裝redis
https://www.runoob.com/redis/redis-install.html
2.pip install channels
3.pip install channels_redis
官方教程鏈接:https://channels.readthedocs.io/en/latest/tutorial/part_1.html
This tutorial is written for Channels 3.0, which supports Python 3.6+ and Django 2.2+.如果版本不符合要求,可以去官方教程里面找對應(yīng)版本的教程铣焊。
1.新建項(xiàng)目和應(yīng)用
在想保存項(xiàng)目的目錄下打開命令行:
新建項(xiàng)目:django-admin startproject mychatsite
切換到剛建的項(xiàng)目mychatsite下:cd mychatsite
新建應(yīng)用:python manage.py startapp chat
2.刪除部分之后步驟不會用到的文件
將chat應(yīng)用下除了init.py和view.py之外的其余文件全部刪除。因?yàn)橹笥貌坏健?br>
刪除后,目錄應(yīng)該是這樣的:
3.在setting文件中添加chat應(yīng)用
4.新建模板文件
在chat應(yīng)用的templates文件夾中新建文件夾chat市怎,chat文件夾下再新建HTML文件index.html和room.html糠排。
index.html寫入以下內(nèi)容:
<!-- chat/templates/chat/index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Chat Rooms</title>
</head>
<body>
What chat room would you like to enter?<br>
<input id="room-name-input" type="text" size="100"><br>
<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)容:
<!-- chat/templates/chat/room.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>Chat Room</title>
</head>
<body>
<textarea id="chat-log" cols="100" rows="20"></textarea><br>
<input id="chat-message-input" type="text" size="100"><br>
<input id="chat-message-submit" type="button" value="Send">
{{ room_name|json_script:"room-name" }}
<script>
const roomName = JSON.parse(document.getElementById('room-name').textContent);
const chatSocket = new WebSocket(
'ws://'
+ window.location.host
+ '/ws/chat/'
+ roomName
+ '/'
);
chatSocket.onmessage = function(e) {
const data = JSON.parse(e.data);
document.querySelector('#chat-log').value += (data.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) {
const messageInputDom = document.querySelector('#chat-message-input');
const message = messageInputDom.value;
chatSocket.send(JSON.stringify({
'message': message
}));
messageInputDom.value = '';
};
</script>
</body>
</html>
5.新建chat/urls.py文件。
該路由文件指向剛剛新建的兩個文件蠢壹。
應(yīng)包含以下代碼:
# 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'),
]
6.在chat/views.py文件中寫入以下代碼:
# 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
})
7.修改mychatsite/urls.py使其可以指向chat/urls.py:
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^chat/', include('chat.urls')),
url(r'^admin/', admin.site.urls),
]
到了這一步嗓违,已經(jīng)搭建好了用來展示聊天室功能的基礎(chǔ)頁面(雖然特別簡陋)。
運(yùn)行 python manage.py runserver后知残,打開鏈接http://127.0.0.1:8000/chat/靠瞎,輸入房間名比庄,即可進(jìn)入一個房間。但是乏盐,聊天室功能還沒有實(shí)現(xiàn)佳窑,所以僅僅是有個頁面的樣子。接下來父能,實(shí)現(xiàn)聊天室功能神凑。
8.集成Channels庫
Django不直接支持WebSocket,所以需要使用Channels庫來支持ws協(xié)議何吝。為了同時處理http和websocket請求溉委,需要用到ASGI,而不是只能處理http的WSGI爱榕。
調(diào)整mysite/asgi.py文件包含以下代碼:
# mysite/asgi.py
import os
from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'mysite.settings')
application = ProtocolTypeRouter({
"http": get_asgi_application(),
# Just HTTP for now. (We can add other protocols later.)
})
需要在根路由配置中指定channels瓣喊,并且配置channel_layers用來支持通信,所以編輯mychatsite/settings.py文件黔酥,在settings.py文件中任意位置添加如下代碼:
ASGI_APPLICATION = 'mychatsite.asgi.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
在已安裝應(yīng)用里面添加channels:
接下來運(yùn)行python manage.py runserver
會發(fā)現(xiàn)
倒數(shù)第二行中藻三,服務(wù)器已經(jīng)不是WSGI的了,被ASGI取代了跪者。
現(xiàn)在的目錄結(jié)構(gòu)是這個樣子的:
8.寫consumers類
Django Channels將處理ws請求的類命名為consumers類棵帽,認(rèn)為是一個個消費(fèi)者。consumers類地位等同于views.py文件中的函數(shù)或類渣玲。簡單來講逗概,consumers類就是用來處理ws請求的,就像views.py中的視圖函數(shù)處理http請求忘衍。
首先逾苫,新建文件chat/consumers.py,此時目錄結(jié)構(gòu)是這樣的:
接下來枚钓,在consumers.py中寫入以下代碼(一步到位隶垮,直接寫的異步處理):
# chat/consumers.py
import json
from channels.generic.websocket import AsyncWebsocketConsumer
class ChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_name = self.scope['url_route']['kwargs']['room_name']
self.room_group_name = 'chat_%s' % self.room_name
# 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 = event['message']
# Send message to WebSocket
await self.send(text_data=json.dumps({
'message': message
}))
說明:
self.scope['url_route']['kwargs']['room_name']:每一個消費(fèi)者都有一個scope,這個scope里面包括的有關(guān)連接的信息秘噪,包括在URL路徑里的參數(shù)等狸吞。
self.room_group_name = 'chat_%s' % self.room_name,用戶可指定組名指煎。
self.accept()接受WebSocket連接蹋偏。如果未在connect()方法內(nèi)調(diào)用accept(),則連接將被拒絕并關(guān)閉至壤。例如威始,對沒有授權(quán)的訪問者可能想拒絕連接。
然后像街,新建chat/routing.py文件指向consumers.py黎棠。就像chat/urls.py指向views.py中的視圖函數(shù)一樣晋渺,Django用routing.py文件指向consumers.py中的類。
在chat/routing.py中寫入以下代碼:
# 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()),
]
使用as_asgi()類方法來為每一個連接的用戶實(shí)例化一個消費(fèi)者類脓斩。
接下來修改mysite/asgi.py文件指向chat/routing.py木西,寫入以下代碼:
# mysite/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", "mysite.settings")
application = ProtocolTypeRouter({
"http": get_asgi_application(),
"websocket": AuthMiddlewareStack(
URLRouter(
chat.routing.websocket_urlpatterns
)
),
})
此時,當(dāng)前端接收到ws請求時随静,就可經(jīng)由路由文件八千,指向consumers.py文件中的類。收發(fā)的消息是json串格式燎猛。一個簡單的聊天室就搭建完成恋捆,當(dāng)然,指的是后端重绷,前端頁面還需設(shè)計(jì)沸停。
聊天室使用方式:
2.python項(xiàng)目文件夾下運(yùn)行python manage.py runserver即可星立。
看到的大多教程都是教如何建立一個聊天室,代碼中體現(xiàn)出來就是建立一個group葬凳。Django Channels還提供了單通道,用來給特定用戶發(fā)送信息的室奏。這樣就可以實(shí)現(xiàn)好友私聊之類的點(diǎn)對點(diǎn)功能火焰。收發(fā)消息邏輯和組相同,接下來寫單人聊天功能胧沫。
9.處理一對一聊天的代碼示例
(名字和上面那個類重復(fù)了昌简,僅僅用來參考,直接復(fù)制粘貼不能運(yùn)行绒怨,還得改改room.html里面發(fā)送和接受消息的格式):
# chat/consumers.py
from channels.generic.websocket import AsyncWebsocketConsumer
import json
from channels.layers import get_channel_layer
from .models import Messages
from channels.db import database_sync_to_async
class ChatConsumer(AsyncWebsocketConsumer):
users = [] #存儲在線列表纯赎,所有用戶共享的變量
history = []#存儲歷史記錄,也可存在數(shù)據(jù)庫南蹂。
async def connect(self):
# 獲取用戶名
room_name = self.scope['url_route']['kwargs']['room_name']
#添加進(jìn)在線用戶列表犬金。添加之前,可以做一系列操作六剥,例如查看用戶是否合法訪問等
self.users.append({'room_name':room_name,'channel_name':self.channel_name})
# 同意連接
await self.accept()
# 檢查是否有歷史未讀消息晚顷,若有,則發(fā)送給用戶(還可以從數(shù)據(jù)庫讀攘婆薄)
message = []
print(self.history)
if len(self.history)>0:
for item in self.history:
#如果歷史消息里這條記錄是發(fā)送給剛登錄的用戶的该默,添加進(jìn)用戶歷史信息列表
if item['To_ID']==room_name:
message.append(item)
# 如果message長度大于零,表示有歷史記錄策彤,
if len(message)>0:
# for item in message:
# self.history.remove(item)
await self.send(text_data=json.dumps({
'message': message
}))
async def disconnect(self, close_code):
#從在線列表中移除后退出
self.users.remove({'room_name':self.scope['url_route']['kwargs']['room_name'],'channel_name':self.channel_name})
await self.close()
# Receive message from WebSocket
async def receive(self, text_data):
text_data_json = json.loads(text_data)['message']
# 存入數(shù)據(jù)庫
# await self.savemsg(text_data_json)
# 往特定channel發(fā)消息栓袖,這邊是寫死的匣摘,前端傳過來的To_ID是test01
To_ID = text_data_json['To_ID']
# 若已經(jīng)登錄,則直接發(fā)送
channel_name = ''
for item in self.users:
if item['room_name'] == To_ID:
channel_name = item['channel_name']
break
# 判斷是否在已登錄記錄中
if channel_name != '':
# Send message to room
await self.channel_layer.send(
channel_name,
{
'type': 'chat_message',
'message': text_data_json,
}
)
print("發(fā)送成功")
else:
# 否則裹刮,存儲到歷史記錄
self.history.append(text_data_json)
print(self.history)
# Receive message from room group
async def chat_message(self, event):
message = event['message']
# Send message to WebSocket音榜。發(fā)送到前端
print(message)
await self.send(text_data=json.dumps({
'message': [message]
}))
@database_sync_to_async
def savemsg(self, text_data_json):
print("save to database")
From_ID = text_data_json['From_ID']
To_ID = text_data_json['To_ID']
Content = text_data_json['Content']
Time = text_data_json['Time']
MSg = Messages.objects.create(From_ID=From_ID, To_ID=To_ID, Content=Content, Time=Time)
MSg.save()
@database_sync_to_async
def readhistorymsg(self, From_ID, UID):
Msg = Messages.objects.filter(From_ID=From_ID,To_ID=UID)
return Msg
在chat/routing.py中新寫一個路由指向這個類即可。