Django 搭建聊天室

準備工作和說明:

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+.如果版本不符合要求,可以去官方教程里面找對應版本的教程。

1.新建項目和應用

在想保存項目的目錄下打開命令行:
新建項目:django-admin startproject mychatsite
切換到剛建的項目mychatsite下:cd mychatsite
新建應用:python manage.py startapp chat

2.刪除部分之后步驟不會用到的文件

將chat應用下除了init.py和view.py之外的其余文件全部刪除。因為之后用不到。
刪除后,目錄應該是這樣的:

image.png

3.在setting文件中添加chat應用

image.png

4.新建模板文件

在chat應用的templates文件夾中新建文件夾chat,chat文件夾下再新建HTML文件index.html和room.html。
index.html寫入以下內容:

<!-- 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寫入以下內容:

<!-- 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文件。

該路由文件指向剛剛新建的兩個文件。
應包含以下代碼:

# 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),
]

到了這一步,已經搭建好了用來展示聊天室功能的基礎頁面(雖然特別簡陋)。
運行 python manage.py runserver后,打開鏈接http://127.0.0.1:8000/chat/,輸入房間名,即可進入一個房間。但是,聊天室功能還沒有實現(xiàn),所以僅僅是有個頁面的樣子。接下來,實現(xiàn)聊天室功能。

8.集成Channels庫

Django不直接支持WebSocket,所以需要使用Channels庫來支持ws協(xié)議。為了同時處理http和websocket請求,需要用到ASGI,而不是只能處理http的WSGI。
調整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)],
        },
    },
}

在已安裝應用里面添加channels:


image.png

接下來運行python manage.py runserver
會發(fā)現(xiàn)


image.png

倒數(shù)第二行中,服務器已經不是WSGI的了,被ASGI取代了。

現(xiàn)在的目錄結構是這個樣子的:


image.png

8.寫consumers類

Django Channels將處理ws請求的類命名為consumers類,認為是一個個消費者。consumers類地位等同于views.py文件中的函數(shù)或類。簡單來講,consumers類就是用來處理ws請求的,就像views.py中的視圖函數(shù)處理http請求。

首先,新建文件chat/consumers.py,此時目錄結構是這樣的:


image.png

接下來,在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']:每一個消費者都有一個scope,這個scope里面包括的有關連接的信息,包括在URL路徑里的參數(shù)等。

self.room_group_name = 'chat_%s' % self.room_name,用戶可指定組名。

self.accept()接受WebSocket連接。如果未在connect()方法內調用accept(),則連接將被拒絕并關閉。例如,對沒有授權的訪問者可能想拒絕連接。

然后,新建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()類方法來為每一個連接的用戶實例化一個消費者類。

接下來修改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
        )
    ),
})

此時,當前端接收到ws請求時,就可經由路由文件,指向consumers.py文件中的類。收發(fā)的消息是json串格式。一個簡單的聊天室就搭建完成,當然,指的是后端,前端頁面還需設計。
聊天室使用方式:

1.首先開啟redis服務,不要關閉這個頁面。(開啟命令看最開始準備工作里面的redis教程)
image.png

2.python項目文件夾下運行python manage.py runserver即可。

看到的大多教程都是教如何建立一個聊天室,代碼中體現(xiàn)出來就是建立一個group。Django Channels還提供了單通道,用來給特定用戶發(fā)送信息的。這樣就可以實現(xiàn)好友私聊之類的點對點功能。收發(fā)消息邏輯和組相同,接下來寫單人聊天功能。

9.處理一對一聊天的代碼示例

(名字和上面那個類重復了,僅僅用來參考,直接復制粘貼不能運行,還得改改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']
        #添加進在線用戶列表。添加之前,可以做一系列操作,例如查看用戶是否合法訪問等
        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ā)送給剛登錄的用戶的,添加進用戶歷史信息列表
                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']

        # 若已經登錄,則直接發(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中新寫一個路由指向這個類即可。

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

相關閱讀更多精彩內容

友情鏈接更多精彩內容