准备工作和说明:
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之外的其余文件全部删除。因为之后用不到。
删除后,目录应该是这样的:
3.在setting文件中添加chat应用
4.新建模板文件
在chat应用的templates文件夹中新建文件夹chat,chat文件夹下再新建HTML文件index.html和room.html。
index.html写入以下内容:
Chat Rooms
What chat room would you like to enter?
room.html写入以下内容:
Chat Room
{{ room_name|json_script:"room-name" }}
5.新建chat/urls.py文件。
该路由文件指向刚刚新建的两个文件。
应包含以下代码:
# chat/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('', views.index, name='index'),
path('/', 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:
接下来运行python manage.py runserver
会发现
倒数第二行中,服务器已经不是WSGI的了,被ASGI取代了。
现在的目录结构是这个样子的:
8.写consumers类
Django Channels将处理ws请求的类命名为consumers类,认为是一个个消费者。consumers类地位等同于views.py文件中的函数或类。简单来讲,consumers类就是用来处理ws请求的,就像views.py中的视图函数处理http请求。
首先,新建文件chat/consumers.py,此时目录结构是这样的:
接下来,在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\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串格式。一个简单的聊天室就搭建完成,当然,指的是后端,前端页面还需设计。
聊天室使用方式:
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中新写一个路由指向这个类即可。