前言
欢迎来到使用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_layer
和self.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
请求。 - 如果它是一个
Websocket
,AuthMiddlewareStack
将从routing
中获取它,并使用当前已经通过身份验证的用户填充连接的scope
,类似与Django的AuthenticationMiddleware
通过当前已经通过验证的用户填充视图函数的请求。 - 接下来,
URLRouter
将根据routing
提供的url将连接路由提供给特定的使用者。
前端:使用websocket
建立即时通信连接
接下来修改前端项目下的views
的ChatView
以建立前后端的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