最近在尝试给我的网站增加多人聊天室的功能,学习了下websocket,发现django可以结合websocket实现,再结合官网文档进行尝试搭建个多人聊天的功能,官网文档链接https://channels.readthedocs.io/en/latest/index.html 其channels库可以实现websocket请求,本次使用的环境是django2.2.4,channels,channesl-redis,以及redis。安装时直接pip install xxx即可。我们先创建一个django项目,命令行python django-admin startproject django_discuss 创建了一个名为django_discuss的django项目了,接下来切进django_discuss目录,使用命令python manage.py startapp inde创建一个名为index的app,具体结构如下
这里注意的是,django2项目创建时没有asgi.py这个文件,这个django3才会有,这个不急,后面会讲,static和templates文件夹分别存储静态资源和前端代码的,在settings.py可以配置下,对应代码
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [os.path.join(BASE_DIR,'templates')],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
以及静态文件路由
STATIC_URL = '/static/'
STATICFILES_DIRS = [
os.path.join(BASE_DIR,'static')
]
将创建的app加入到settings.py中,如图所示
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'index',
]
进入django_discuss/urls.py中,设置路由
from django.contrib import admin
from django.urls import path,include
urlpatterns = [
path('admin/', admin.site.urls),
path('',include('index.urls')),
]
进入index/views.py中,后端视图
from django.shortcuts import render
# Create your views here.
def index(request):
return render(request,'index.html')
在templates/index.html中
{% load static %}
<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>
新建index/urls.py
from django.urls import path
from . import views
urlpatterns = [
path('chat/',views.index,name='index'),
]
现在可以正常运行项目了,python manage.py runserver运行,目前功能只是一个房间号的输入框,还没用到channels,具体功能是用户输入房间号立马跳转到房间号实现对话效果,加一个对话框视图,将一下代码放入templates/room.html中
{% load static %}
<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>
这里ws表示是websocket请求,因此后端根据websocket的链接配置对应路由,在index.urls.py中如下
from django.urls import path
from . import views
urlpatterns = [
path('chat/',views.index,name='index'),
path('chat//',views.room,name='room')
]
传递前端输入的房间名,对应后端视图代码即index/views.py中
from django.shortcuts import render
# Create your views here.
def index(request):
return render(request,'index.html')
def room(request,room_name):
return render(request,'room.html',context={'room_name':room_name})
先说下基本知识,websocket中有4个事件,open,message,error,close.open是服务器一旦响应了websocket请求,立马触发open事件,open事件对应的回调函数为onopen,而message为websocket消息,来自其对应服务器的数据,其回调函数为onmessage,error为处理以为故障触发,close为websocket连接关闭时触发。而websocket对象有2个方法send和close,这个在django后端视图消费者部分体现明显。聊天室大概原来我们说下,首先用户前端输入房间号回车后,进入聊天室的界面,通过前端new websocket路由连接后端视图,前端中的ws.onmessage监听后端服务器传来的消息,一有消息则将聊天内容显示在对话框上,对于前端用户输入的消息,则前端绑定输入框,使用send方法传给django后端,这里先将输入内容通过SON.stringify转化为字符串再传给后端,这里重要的就是后端接受消息和发送消息部分了。当 Django 接受一个 HTTP 请求时,它会参考根 URLconf 来查找视图函数,然后调用视图函数来处理请求。同样,当 Channels 接受 WebSocket 连接时,它会参考根路由配置来查找消费者,然后调用消费者上的各种函数来处理来自连接的事件。而这个消费者即后端接受消息和发送消息的重要部分了。
在settings.py中加入channels,
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'index',
'channels'
]
新建django_discuss/asgi.py(django2得新建,django3自带),代码如下
import os
import django
import index.routing
from channels.http import AsgiHandler
from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
from channels.security.websocket import AllowedHostsOriginValidator
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'django_discuss.settings')
application = ProtocolTypeRouter({
"http": AsgiHandler,
# Just HTTP for now. (We can add other protocols later.)
"websocket": AllowedHostsOriginValidator(
AuthMiddlewareStack(
URLRouter(
index.routing.websocket_urlpatterns
)
)
),
})
类似于普通的路由配置,只不过这里是websocket根路由,很好理解,和django_discuss中的urls的根路由一个道理,接着在settings.py将channels指向配置好了的根路由
WSGI_APPLICATION = 'django_discuss.wsgi.application'
#新增
ASGI_APPLICATION = 'django_discuss.asgi.application'
接下来开始写消费者后端了,创建index/consumers.py,代码如下
import json
from channels.generic.websocket import WebsocketConsumer
from channels.generic.websocket import AsyncWebsocketConsumer
from asgiref.sync import async_to_sync
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
#加入room group
await self.channel_layer.group_add(
self.room_group_name,self.channel_name
)
await self.accept()
async def disconnect(self, close_code):
await self.channel_layer.group_discard(
self.room_group_name, self.channel_name
)
async def receive(self, text_data):
text_data_json = json.loads(text_data)
message = text_data_json['message']
await self.channel_layer.group_send(
self.room_group_name,
{
'type':'chat_message',
'message':message
}
)
async def chat_message(self,event):
message = event['message']
await self.send(text_data=json.dumps({
'message': message
}))
这是一个异步的websocket消费者,为了确保同一房间多个实例相互通信,需要使用redis作为后备存储的通道层,在settings.py中增加
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'channels_redis.core.RedisChannelLayer',
'CONFIG': {
"hosts": [('127.0.0.1', 6379)],
},
},
}
上面消费者中connect首先接受前端的websocket连接,其接受一个房间号参数,获取房间号参数,将其房间名称作为channels组名称,实现了对于同一组的可以相互通信,对于关闭连接,只需移除房间号即可,receive函数接受前端send发送过来的数据包,通过chat_message函数将数据包发送给前端对话框中,这样就实现了通信循环。
然后创建index/routing.py,设置该app下的websocket路由,代码如下,
from django.urls import re_path
from . import consumers
websocket_urlpatterns = [
re_path(r'ws/chat/(?P\w+)/$', consumers.ChatConsumer),
]
先启动redis服务,然后运行代码,2个浏览器实现同一房间号进行通信,如图1,2
实现了2个浏览器之间的简单通信,当然,你可以在对话框加个用户标识,设置用户登录后对话,界面优化等功能都可以,这里只是粗糙的简易的实现方式而已。
点击此处登录后即可评论