讲解路线:聊天室原理——>多人聊天——>房间活动——>点对点聊天——>消息推送
一、聊天室原理
即时通讯在如今已经十分常见,大到QQ、微信,小到网站客服,这其实是同一个“聊天室”原理。聊天室采用WebSocket技术,下面来聊一聊WebSocket技术。
1. WebSocket原理
与http连接不同,websocket是长连接,一次握手即可传输消息,每个连接有一个唯一的身份标识
2. 多人聊天原理
多人聊天则是通过groupid将一个个的连接进行分组。客户端要发送聊天信息给服务器端,服务器在和该客户端同一组的组内进行消息广播,这样,每个连接都能接收到消息。
点对点聊天原理
相当于这两个人是一组
消息推送
相当于一个人一组,这同时也使用于特殊定制通知服务。
二、代码实战
教材 https://channels.readthedocs.io/en/stable/introduction.html
django 同时支持http和websocket请求,只不过涉及到的配置文件不同
1. 安装
python -m pip install -U channels
2. 在Django中注册channels
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
]
3. 使用asgi服务器与wsgi服务器
asgi.py
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'GameDesc.settings')
application = get_asgi_application()
wsgi.py
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'GameDesc.settings')
application = get_wsgi_application()
4. 新启项目Room并进行配置
4.1 新建项目
python3 manage.py startapp Room
4.2 配置settings
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'Room',
]
4.3 新建文件夹customers和routings.py
views文件夹下为http类型接口处理的视图函数,customers文件夹下处理websocket请求
urls.py为http类型的路由,routings为websocket类型的路由
6. 添加总路由
在项目文件夹(有settings的文件夹)下新建routings.py,作为总路由
routings.py
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import Room.routing
application = ProtocolTypeRouter({
# (http->django views is added by default)
'websocket': AuthMiddlewareStack(
URLRouter(
Room.routing.websocket_urlpatterns
)
),
})
7. 设置关于channels的settings
目录结构
# Channels
ASGI_APPLICATION = 'GameDesc.routing.application'
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
8. 创建视图文件
8.1 预备知识1
基本的customers结构(同步)
# chat/consumers.py
import json
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
class ChatConsumer(WebsocketConsumer):
# 建立连接
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
async_to_sync(self.channel_layer.group_add)(
self.room_group_name,
self.channel_name
)
# 接受连接,不接收不连接
self.accept()
# 断开连接
def disconnect(self, close_code):
# Leave room group
async_to_sync(self.channel_layer.group_discard)(
self.room_group_name,
self.channel_name
)
# Receive message from WebSocket 从客户端发送的包中获取消息
def receive(self, text_data):
text_data_json = json.loads(text_data)
# 从包中获取消息,前端的字段为message
message = text_data_json['message']
# Send message to room group 将要广播的消息添加至包中,字段为message
async_to_sync(self.channel_layer.group_send)(
self.room_group_name,
{
'type': 'chat_message',
'message': message
}
)
# Receive message from room group
def chat_message(self, event):
message = event['message']
# Send message to WebSocket 发送到ws连接中
self.send(text_data=json.dumps({
'message': message
}))
8.2 预备知识2
基本的customers结构(异步),和同步一样
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
}))
8.3 代码实战
回到customers文件夹下,创建ChatCustomers.py
8.3.1 说明:
(1)由于是教学版,故省略较多逻辑检查
(2)项目需要,故返回了历史聊天记录,但是实际上是不需要储存聊天记录的,就像加入QQ群聊,只能获取加入之后的消息,是不会获取到加入之前的聊天记录的
(3)这一环节没有涉及到mysql等关系型数据库的操作,故可以使用异步提高效率,但是涉及到对mysql数据库增删改查等操作时,不能使用异步,只能使用同步,否则会报错(亲测)
8.3.2 ChatCustomers.py
有了上面的知识,相信你一定可以看明白啦
from GameDesc.settings import MAX_REDIS_SAVE_TIME
from channels.generic.websocket import AsyncWebsocketConsumer
from GameDesc.redis_tools.redis_message import redis_get_room_message, redis_save_room_message # 此处是redis存储消息,是为了获取历史消息,可以不管
import json
import datetime
import time
# 进入房间后即可点击加入聊天室 注意检查用户是否登录
class RoomChatConsumer(AsyncWebsocketConsumer):
async def connect(self):
self.room_id = self.scope['url_route']['kwargs']['room_id'] # 获取传输的房间Id
self.user_id = self.scope['url_route']['kwargs']['user_id'] # 获取传输的user_id
self.room_group_name = 'chat_%s' % self.room_id # 添加分组名以chat_开头
# Join room chat group
await self.channel_layer.group_add(
self.room_group_name, # 房间号标识 用于分组
self.channel_name, # 个人标识
)
await self.accept()
# 发送加入房间消息并且获取历史聊天记录
flag, old_messages = redis_get_room_message(room_group_name=self.room_group_name)
if flag == False:
message = {
"_id": '2',
"groupId": self.room_id,
"nickName": self.user_id,
"avatarUrl": "",
"textContent": "未获得历史消息",
"user_id": "oKYr74nbN8kRyqZZXGAHb-0xWuzQ1", # user_id
"sendTime": time.ctime(),
"sendTimeTS": time.ctime()
}
old_messages.append(message)
else:
message = {
"_id": '1',
"groupId": self.room_id,
"nickName": self.user_id,
"avatarUrl": "",
"textContent": "{0} 已加入聊天室...".format(self.user_id),
"user_id": "oKYr74nbN8kRyqZZXGAHb-0xWuzQ1",
"sendTime": time.ctime(),
"sendTimeTS": time.ctime()
}
old_messages.append(message)
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': old_messages # 把发送的信息放到event['message']字段里面 ,返回所有信息,数组形式
}
)
async def disconnect(self, close_code):
self.room_id = self.scope['url_route']['kwargs']['room_id']
self.user_id = self.scope['url_route']['kwargs']['user_id']
# 退出聊天室向组内所有人广播
message = {
"_id": '0',
"groupId": self.room_id,
"nickName": self.user_id,
"avatarUrl": "",
"textContent": "{0} 已退出聊天室...".format(self.user_id),
"user_id": "oKYr74nbN8kRyqZZXGAHb-0xWuzQ1",
"sendTime": time.ctime(),
"sendTimeTS": time.ctime()
}
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message # 把发送的信息放到event['message']字段里面
}
)
# 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']
redis_save_room_message(self.room_group_name, message, MAX_REDIS_SAVE_TIME) # 时间暂定
# Send message to room group
await self.channel_layer.group_send(
self.room_group_name,
{
'type': 'chat_message',
'message': message, # 把发送的信息放到event['message']字段里面
}
)
# Receive message from room group
async def chat_message(self, event):
message = event['message']
# Send message to WebSocket 将前面函数返回的消息发送到ws连接上
await self.send(text_data=json.dumps({
'message': message,
}))