使用Django-channels/Vue3&websocket实现一个即时聊天室—Part 3 channels初探

前言

欢迎来到使用Django&Channels、Vue3&WebSocket实现一个即时聊天室教程的第三部分。本次教程主要是编写聊天室的接口以及channels的基本使用。

这是本次教程的待办事项:

  • 创建聊天室并编写模型
  • 聊天室列表展示
  • 获取聊天室详情
  • channels的配置
  • channels与websocket的通信

创建聊天室并编写模型

创建app

accounts子app一样,输入以下命令创建:

shell 复制代码
python manage.py startapp chat

settings.py中注册

python 复制代码
INSTALLED_APPS = [
    "django.contrib.admin",
    "django.contrib.auth",
    "django.contrib.contenttypes",
    "django.contrib.sessions",
    "django.contrib.messages",
    "django.contrib.staticfiles",
    # 添加新app
    "accounts",
    "chat",
    # 第三方库,如:rest-framework、channels等...
]

编写模型

我们需要创建一个模型来存放聊天室。在server/chat/models.py下创建聊天室模型:

python 复制代码
from django.db import models
from accounts.models import User

class Room(models.Model):
    name = models.CharField(max_length=255)
    slug = models.SlugField(unique=True)
    
    def get_absolute_url(self):
        return f"/{self.slug}/"

    def __str__(self) -> str:
        return self.name

slug即标签的意思,主要用于标记单个数据项,多用于链接地址;如表示某个聊天室的网络地址:http://localhost:8000/rooms/game/,game就是slug字段存储的数据。

编写完成后,让我们合并数据库:

shell 复制代码
python manage.py makemigrations
python manage.py migrate

添加示例数据

在展示聊天室列表之前,让我们先往数据库内添加几条数据:

让我们打开终端,在python shell中键入以下命令:

python 复制代码
# 进入shell
python manage.py shell
# 在python终端下执行命令
>>> from chat.models import Room
>>> items = [
    Room(name='Coding',slug='coding'),
    Room(name='Games',slug='games'),
    Room(name='Work',slug='work'),
    Room(name='Life',slug='life')
]
# 使用bulk_create批量创建数据,注意输入的数据需要保证与数据模型字段对应
>>> Room.objects.bulk_create(items)
[<Room: Coding>, <Room: Games>, <Room: Work>, <Room: Life>]
>>> all_rooms = Room.objects.all()
>>> all_rooms
<QuerySet [<Room: Coding>, <Room: Games>, <Room: Work>, <Room: Life>]>

我们可以在admin页面中查看结果:

再查看一下单条数据的内容是否正确:

非常完美,我们已经成功插入了数据,接下来让它们在前端页面展示吧!

聊天室功能实现

要将数据展示至前端,需要先制作数据接口供前端请求,下面让我们编写数据接口:

构建聊天室api

构建序列化器

python 复制代码
# server/chat/serializers.py
from rest_framework import serializers
from .models import Room


class RoomSerializer(serializers.ModelSerializer):
    class Meta:
        model = Room
        fields = ("name", "slug", "get_absolute_url")

构建接口

python 复制代码
# server/chat/views.py
from rest_framework.decorators import api_view, permission_classes
from rest_framework.permissions import IsAuthenticated
from rest_framework.response import Response
from rest_framework import status

from .models import Room
from .serializers import RoomSerializer


@api_view(['GET'])
@permission_classes([IsAuthenticated])
def room_list(requset):
    rooms = Room.objects.all()
    serializer = RoomSerializer(rooms, many=True)
    serializer_data = serializer.data

    return Response({
        "message": "获取房间列表成功!",
        "rooms": serializer_data
    },status=status.HTTP_200_OK)


@api_view(['GET'])
@permission_classes([IsAuthenticated])
def room_detail(request, slug):
    room = Room.objects.get(slug=slug)
    roomSerializer = RoomSerializer(room)
    room_detail = roomSerializer.data
    return Response({
        "message": "进入房间成功!", 
        "room": room_detail
    },status=status.HTTP_200_OK)

这里定义了两个API视图:

  • 聊天室列表(room_list),用于获取聊天室列表以及聊天室的详细信息;
  • 聊天室详情(room_detail),用户获取某个聊天室的数据,需要通过前端传递slug参数来访问特定聊天室。

注:这里使用了IsAuthenticated权限类来对请求进行验证,要求用户在请求时进行认证。

准备路由

python 复制代码
# server/chat/urls.py

from django.urls import path

from . import views

urlpatterns = [
    path("", views.room_list, name="rooms_list"),
    path("<slug:slug>/", views.room_detail, name="room_detail"),
]

还需要在主路由上挂载app的路由:

python 复制代码
# server/chat_api/urls.py
from django.contrib import admin
from django.urls import path, include

urlpatterns = [
    path("admin/", admin.site.urls),
    path("auth/", include("accounts.urls")),
    # 挂载chat
    path("chat/", include("chat.urls"))
]

测试接口

测试接口前,先运行登录接口获取Token

测试获取聊天室列表接口:

测试接口前为请求添加请求头

测试接口:

测试聊天室详情接口:

测试大获成功!现在我们编写前端页面将数据展示出来。

构建聊天室

页面设计

RoomsView
html 复制代码
<template>
  <main>
    <div class="p-10 text-center lg:p-20">
      <h1 class="text-3xl text-white lg:text-6xl">Rooms</h1>
    </div>

    <div class="w-full flex flex-wrap items-center">
      <div class="w-full p-3 lg:w-1/4">
        <div class="p-4 bg-white shadow rounded-xl text-center">
          <h2 class="mb-5 text-2xl font-semibold">Coding</h2>
          <router-link
            :to=""
            class="px-5 py-3 block rounded-xl text-white bg-teal-600 hover:bg-teal-700"
            >加入房间</router-link
                   >
        </div>
      </div>
    </div>
  </main>
</template>
ChatView
html 复制代码
<template>
	<main>
		<div class="p-10 text-center lg:p-20">
			<h1 class="text-3xl text-white lg:text-6xl">Coding</h1>
		</div>

		<div class="mx-4 p-4 bg-white rounded-xl lg:w-2/4 lg:mx-auto">
			<div class="chat-messages space-y-3 mb-3">
				<div class="p-4 bg-gray-200 rounded-xl">
					<p class="font-semibold">sanapri</p>
					<p>hello</p>
				</div>
			</div>
      <div class="chat-messages space-y-3 mb-3">
				<div class="p-4 bg-gray-200 rounded-xl">
					<p class="font-semibold">cerelise</p>
					<p>hi</p>
				</div>
			</div>
		</div>

		<div class="mt-6 mx-4 p-4 bg-white rounded-xl lg:w-2/4 lg:mx-auto">
			<div class="flex">
				<input
					type="text"
					class="flex-1 mr-3 focus:outline-none"
					placeholder="请发送消息..."
				/>
				<button
					class="px-5 py-3 rounded-xl text-white bg-teal-600 hover:bg-teal-700"
				>
					发送
				</button>
			</div>
		</div>
	</main>
</template>

添加页面路由

js 复制代码
{
	path: '/rooms',
	name: 'rooms',
	component: RoomsView,
},
{
	path: '/chat/:room_slug',
	name: 'chat',
	component: ChatView,
},

添加并测试请求(RoomsView)

在测试前先修改一下错误:在userAxios.js下:

js 复制代码
instance.interceptors.request.use(
		(config) => {
			const token = localStorage.getItem('user.token')
			if (token) {
        // 修改为Authorization
				config.headers['Authorization'] = 'Token ' + token
			}
			return config
		},
		(error) => {
			return Promise.reject(error)
		}
	)

修改RoomsView,使聊天室列表展示出来:

vue 复制代码
<template>
	<main>
		<div class="p-10 text-center lg:p-20">
			<h1 class="text-3xl text-white lg:text-6xl">Rooms</h1>
		</div>
		<div class="w-full flex flex-wrap items-center">
			<div v-for="room in rooms" class="w-full p-3 lg:w-1/4">
				<div class="p-4 bg-white shadow rounded-xl text-center">
					<h2 class="mb-5 text-2xl font-semibold">{{ room.name }}</h2>
					<router-link
						:to="'chat' + room.get_absolute_url"
						class="px-5 py-3 block rounded-xl text-white bg-teal-600 hover:bg-teal-700"
						>加入房间</router-link
					>
				</div>
			</div>
		</div>
	</main>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import useAxios from '../composables/useAxios'

const axios = useAxios()

const rooms = ref([])

function getRoomList() {
	axios.get('chat/').then((res) => {
		console.log(res)
		rooms.value = res.rooms
	})
}

onMounted(() => {
	getRoomList()
})
</script>

修改完成后,启动项目,用浏览器打开http:localhost:3000查看结果:

如上图所示,聊天室列表已成功展示!

注:由于ChatView涉及到channels和websocket的使用,该页面将于下一节讲解

Channels初探

Channels是一个基于Django框架的基础上研发的一个功能库,添加了对Websocket、MQTT(消息队列遥感传输)、聊天机器人等实时功能的支持。在Django,Channels被构建为ASGI服务器。而且它允许开发者选择如何编写代码------使用同步、异步或者两者混搭的方式编写Django视图函数。

值得注意的是,虽然与普通的Django项目不同,使用Channels编写的项目需要ASGI服务支持而不是WSGI,但是Channels并不是Django现有的请求/响应模型的替代,仅仅只是对原有框架的扩展。

为什么需要ASGI服务?

一般使用Django开发的web项目都是使用Django自带的WSGI(web服务器网关接口),它是Python应用处理请求的接口。但是为了处理异步应用,我们需要使用另一个接口:ASGI,即异步服务器网关接口,使用它可以处理websocket请求。

在正式使用channels之前,我们需要先为项目配置ASGI服务

配置ASGI服务

settings.py

首先,在settings.py中,我们找到WSGI_APPLICATION的下方,添加上ASGI服务指向:

python 复制代码
# server/chat_api/settings.py

ASGI_APPLICATION = "chat_api.asgi.application"

注意:要正确填写你的项目名称

现在,将Channels添加到INSTALLED_APP

python 复制代码
INSTALLED_APPS = [
	...
    "channels",
]

asgi.py

在asgi.py中,添加以下代码:

python 复制代码
import os
from channels.routing import ProtocolTypeRouter
from django.core.asgi import get_asgi_application

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'project_name.settings')

application = ProtocolTypeRouter({
    "http":get_asgi_application(),
    # 现在已经添加了HTTP,我们还可以在该配置中添加其他通信协议
})

什么是ProtocolTypeRouter?

ProtocolTypeRouter是channels使用的一种特殊路由,它允许开发者根据网络请求类型将进入django应用程序的请求重定向到合适的视图。这对于需要支持多种协议类型(如HTTP和Websocket)的应用程序非常有用。ProtocolTypeRouter还允许开发者将每个协议类型映射到对应的视图集中,从而使不同协议各有所属,并帮助开发者在客户端和服务器之间创建安全的通信通道。

Channel Layers

channel layers是一种允许django应用的多个实例进行通信和交换消息的机制。通道层(channnel layers)通常在构建分布式应用程序中使用,因为它们不需要所有消息都通过数据库。

请注意,这里我们可以通过以下两种方式设置通道层:

  • InMemoryChannelLayer
  • Channels_redis

在这里,我会介绍两种设置通道层方式:

InMemoryChannelLayer

makefile 复制代码
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels.layers.InMemoryChannelLayer"
    }
}

如果应用只是用于测试和本地开发的话,我们可以选择使用内存层中的通道,但是您不应该在生产环境下使用该层,因为:

内存通道层将每个进程作为单独的层进行操作。这意味着不可能进行跨进程消息传递。由于通道层的核心价值是提供分布式消息传递,因此内存使用将导致性能下降,并最终在多实例环境中导致数据丢失等问题。

channels_redis

Channels_redis是官方提供的通道层,使用Redis作为其消息存储的载体。使用Redis,我们可以将数据存储在其缓存实例中,并直接从运行Redis服务的服务器的内存中进行搜索,而不是直接从数据库中查询。一通操作下来,我们可将聊天消息发送到接收者之前将它保存在消息队列中。

设置Channels_redis之前,您需要在本地安装一个redis。根据不同的操作系统,可以使用不同的方法安装redis。为此,您可以查看官方文档或者问度娘。

下一步,在虚拟环境(env)下,安装channels_redis包,以便Channels知道接下来如何与redis进行交互。

shell 复制代码
pip install channels_redis

接下来是最后一步,在settings.py中添加CHANNEL_LAYERS配置:

python 复制代码
# server/chat_api/settings.py
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [("127.0.0.1", 6379)],
        },
    },
}

由于我们处于开发阶段,而且正在构建一个比较简单的聊天室应用程序,因此我在这不会使用channels_redis的方法。

好了,现在我们已经完成了ASGI的配置,让我们继续下一部分。

后端:初始化channels

consumers.py

现在让我们使用Channels给Django应用添加Websocket。与Django中接收和处理HTTP请求的views类似,在Channels中有一个叫consumers的视图集合,用于Channels接收和处理同步/异步请求,可以让Django应用具备处理Websocket连接的能力。

接下来在chat应用中创建一个名为consumers.py的文件,并输入以下内容:

python 复制代码
# server/chat_api/consumers.py
from channels.generic.websocket import AsyncWebsocketConsumer

class ChatConsumer(AsyncWebsocketConsumer):
    async def connect(self):
        # 从 URL 路由中提取房间名称
        self.room_name = self.scope["url_route"]["kwargs"]["room_name"]
        # 使用提取的房间名称为此房间创建一个组名称
        self.room_group_name = "chat_%s" % self.room_name
        # 将通道层(Websocket连接)添加到组中,让其可以将消息广播
        await self.channel_layer.group_add(self.room_group_name,
                                           self.channel_name)
        # 接受来自Websocket的连接
        await self.accept()

    async def disconnect(self, close_code):
        # 连接正在关闭的时候,删除前后端之间的通道
        await self.channel_layer.group_discard(self.room_group_name,
                                               self.channel_name)

这段代码定义通过继承AsyncWebsocketConsumer实现的ChatConsumer类。该类视图函数负责处理聊天室的Websocket连接。当建立连接时,它会将频道(连接)添加到聊天室特定的组中,以便可以向该聊天室中的所有参与者广播消息。当连接关闭时,它会从组中删除通道。

Channels允许异步处理Websocket连接,但使用者类是同步的。您在安装Django的时候,包含ASGI基础库的asgiref包会作为依赖包自动安装。它提供的sync模块用于封装要在同步消费者中使用的异步通道层方法。ChatConsumer类封装了Websocket处理模型,允许其他应用程序在此基础上构建。

任何Consumer都有self.channel_layerself.chanel_name属性,它们分别是指向通道层实例和特定通道名称的指针。

routing.py

与Django的urls.py具有连通views.py的路由类似,Channals也有一个routing.py用于连通consumers.py。因此,我们继续在chat应用中创建routing.py

python 复制代码
from django.urls import path

from . import consumers

websocket_urlpatterns = [
    path("ws/<str:room_name>/", consumers.ChatConsumer.as_asgi()),
]

as_asgi()方法类似我们在使用基于类视图时调用的as_view()方法。它的作用是返回一个ASGI应用程序。

asgi.py

接下来,跳转回chat_api/目录下,对asgi.py进行以下修改:

python 复制代码
import os
from django.core.asgi import get_asgi_application

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter

from chat import routing

os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'server.settings')

application = ProtocolTypeRouter({
    "http":
    get_asgi_application(),
    "websocket":
    AuthMiddlewareStack(URLRouter(routing.websocket_urlpatterns))
})
  • 上面的代码将主路由配置指向chat应用中的routing模块。这意味着当与开发服务器(Channels的开发服务器)建立连接时,ProtocolTypeRouter会检查它是普通HTTP请求还是Websocket请求。
  • 如果它是一个WebsocketAuthMiddlewareStack将从routing中获取它,并使用当前已经通过身份验证的用户填充连接的scope,类似与Django的AuthenticationMiddleware通过当前已经通过验证的用户填充视图函数的请求。
  • 接下来,URLRouter将根据routing提供的url将连接路由提供给特定的使用者。

前端:使用websocket建立即时通信连接

接下来修改前端项目下的viewsChatView以建立前后端的Websocket连接:

js 复制代码
<template>...</template>
...
<script setup>
  import { useRoute } from 'vue-router'
  import { onMounted, ref } from 'vue'
  
  const chatSocket = ref('')
  const route = useRoute()

  // 获取当前聊天室连接
  const room_slug = route.params.room_slug

  function initChatSocket() {
    chatSocket.value = new WebSocket(
      'ws://127.0.0.1:8000/ws/' + route.params.room_slug + '/'
    )

    chatSocket.value.onmessage = function (e) {
      console.log('onmessage')
    }

    chatSocket.value.onclose = function (e) {
      console.log('onclose')
    }
  }

  onMounted(async () => {
    initChatSocket()
  })
</script>

script中,创建了一个Websocket连接到指定聊天室。这里使用从route中获取的参数room_slug,作为进入特定聊天室的凭证,向后端发起Websocket请求。

在后端中查看连接测试结果:

现在,前后端的websocket连接已经成功建立!

写在最后

下一章节我会完善聊天功能,并将聊天记录存储至数据库,完成聊天室的制作。

可能我对Channels的认知并不是很透彻,理解有错误的地方欢迎批评指正!

本章节的代码:github

相关推荐
GoodStudyAndDayDayUp4 小时前
初入 python Django 框架总结
数据库·python·django
blues_C2 天前
十三、【核心功能篇】测试计划管理:组织和编排测试用例
vue.js·django·测试用例·drf·测试平台
恸流失3 天前
DJango项目
后端·python·django
编程大全3 天前
41道Django高频题整理(附答案背诵版)
数据库·django·sqlite
网安小张3 天前
解锁FastAPI与MongoDB聚合管道的性能奥秘
数据库·python·django
KENYCHEN奉孝3 天前
Pandas和Django的示例Demo
python·django·pandas
老胖闲聊3 天前
Python Django完整教程与代码示例
数据库·python·django
noravinsc3 天前
django paramiko 跳转登录
后端·python·django
践行见远3 天前
django之请求处理过程分析
数据库·django·sqlite
声声codeGrandMaster3 天前
Django之表格上传
后端·python·django