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/,输入房间名,即可进入一个房间。但是,聊天室功能还没有实现,所以仅仅是有个页面的样子。接下来,实现聊天室功能。

8.集成Channels库

Django不直接支持WebSocket,所以需要使用Channels库来支持ws协议。为了同时处理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
会发现


image.png

倒数第二行中,服务器已经不是WSGI的了,被ASGI取代了。

现在的目录结构是这个样子的:


image.png

8.写consumers类

Django Channels将处理ws请求的类命名为consumers类,认为是一个个消费者。consumers类地位等同于views.py文件中的函数或类。简单来讲,consumers类就是用来处理ws请求的,就像views.py中的视图函数处理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路径里的参数等。

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

self.accept()接受WebSocket连接。如果未在connect()方法内调用accept(),则连接将被拒绝并关闭。例如,对没有授权的访问者可能想拒绝连接。

然后,新建chat/routing.py文件指向consumers.py。就像chat/urls.py指向views.py中的视图函数一样,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文件中的类。收发的消息是json串格式。一个简单的聊天室就搭建完成,当然,指的是后端,前端页面还需设计。
聊天室使用方式:

1.首先开启redis服务,不要关闭这个页面。(开启命令看最开始准备工作里面的redis教程)
image.png

2.python项目文件夹下运行python manage.py runserver即可。

看到的大多教程都是教如何建立一个聊天室,代码中体现出来就是建立一个group。Django Channels还提供了单通道,用来给特定用户发送信息的。这样就可以实现好友私聊之类的点对点功能。收发消息逻辑和组相同,接下来写单人聊天功能。

9.处理一对一聊天的代码示例

(名字和上面那个类重复了,仅仅用来参考,直接复制粘贴不能运行,还得改改room.html里面发送和接受消息的格式):

# 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 = []#存储历史记录,也可存在数据库。
    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()


        # 检查是否有历史未读消息,若有,则发送给用户(还可以从数据库读取)
        message = []
        print(self.history)
        if len(self.history)>0:
            for item in self.history:
                #如果历史消息里这条记录是发送给刚登录的用户的,添加进用户历史信息列表
                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']
        # 存入数据库
        # await self.savemsg(text_data_json)

        # 往特定channel发消息,这边是写死的,前端传过来的To_ID是test01
        To_ID = text_data_json['To_ID']

        # 若已经登录,则直接发送
        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("发送成功")
        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。发送到前端
        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中新写一个路由指向这个类即可。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,734评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,931评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,133评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,532评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,585评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,462评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,262评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,153评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,587评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,792评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,919评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,635评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,237评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,855评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,983评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,048评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,864评论 2 354

推荐阅读更多精彩内容