时间:2023-07-06 11:36:02 | 来源:网站运营
时间:2023-07-06 11:36:02 来源:网站运营
Django实战:channels + websocket四步打造个在线聊天室(附动图):Channels是Django团队研发的一个给Django提供websocket支持的框架,它同时支持http和websocket多种协议。使用channels可以让你的Django应用拥有实时通讯和给用户主动推送信息的功能。本文将教你如何使用channels + websocket打造个在线聊天室。一共只有四步,你可以轻松上手并学会。项目中大部分代码是基于channels的官方文档的,加入了些自己的理解,以便新手学习使用。 pip install django==3.2.3 pip install channels==3.0.3
修改settings.py, 将channels和chat加入到INSTALLED_APPS里,并添加相应配置,如下所示: INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', 'django.contrib.contenttypes', 'django.contrib.sessions', 'django.contrib.messages', 'django.contrib.staticfiles', 'channels', # channels应用 'chat', ] # 设置ASGI应用 ASGI_APPLICATION = 'myproject.asgi.application' # 设置通道层的通信后台 - 本地测试用 CHANNEL_LAYERS = { "default": { "BACKEND": "channels.layers.InMemoryChannelLayer" } }
注意 :本例为了简化代码,使用了InMemoryChannelLayer做通道层(channel_layer)的通信后台,实际生产环境中应该需要使用redis作为后台。这时你还需要安装redis和channels_redis,然后添加如下配置: # 生产环境中使用redis做后台,安装channels_redis CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [("127.0.0.1", 6379)], #或"hosts": [os.environ.get('REDIS_URL', 'redis://127.0.0.1:6379/1')], }, }, }
最后将chat应用的urls.py加入到项目urls.py中去,这和常规Django项目无异。 # myproject/urls.py from django.conf.urls import include from django.urls import path from django.contrib import admin urlpatterns = [ path('chat/', include('chat.urls')), path('admin/', admin.site.urls), ]
第二步 编写聊天室页面 # chat/urls.py from django.urls import path from . import views urlpatterns = [ path('', views.index, name='index'), path('<str:room_name>/', views.room, name='room'), ] # 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 })
接下来我们编写两个模板文件index.html和room.html。它们的路径位置如下所示: chat/ __init__.py templates/ chat/ index.html room.html urls.py views.py
index.html内容如下所示。它也基本不涉及websocket,就是让用户输入聊天室后进行跳转。 <!-- chat/templates/chat/index.html --> <!DOCTYPE html> <html> <head> <meta charset="utf-8"/> <title>Chat Rooms</title> </head> <body> 请输入聊天室名称: <input id="room-name-input" type="text" size="100"> <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>
room.html内容如下所示。为了帮助你理解前后端是怎么实现websocket实时通信的,我给每行js代码添加了注释,这对于你理解前端如何发送websocket的请求,如果处理后端发过来的websocket消息至关重要。 <!DOCTYPE html><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); // 根据roomName拼接websocket请求地址,建立长连接 // 请求url地址为/ws/chat/<room_name>/ const wss_protocol = (window.location.protocol == 'https:') ? 'wss://': 'ws://'; const chatSocket = new WebSocket( wss_protocol + window.location.host + '/ws/chat/' + roomName + '/' ); // 建立websocket连接时触发此方法,展示欢迎提示 chatSocket.onopen = function(e) { document.querySelector('#chat-log').value += ('[公告]欢迎来到' + roomName + '讨论群。请文明发言!/n') } // 从后台接收到数据时触发此方法 // 接收到后台数据后对其解析,并加入到聊天记录chat-log chatSocket.onmessage = function(e) { const data = JSON.parse(e.data); document.querySelector('#chat-log').value += (data.message + '/n'); }; // websocket连接断开时触发此方法 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(); } }; // 每当点击发送消息按钮,通过websocket的send方法向后台发送信息。 document.querySelector('#chat-message-submit').onclick = function(e) { const messageInputDom = document.querySelector('#chat-message-input'); const message = messageInputDom.value; //注意这里:先把文本数据转成json格式,然后调用send方法发送。 chatSocket.send(JSON.stringify({ 'message': message })); messageInputDom.value = ''; }; </script></body></html>
此时如果你使用python manage.py runserver命令启动测试服务器,当你访问一个名为/hello/的房间时,你将看到如下页面: # chat/routing.py from django.urls import re_path from . import consumers websocket_urlpatterns = [ re_path(r'ws/chat/(?P<room_name>/w+)/$', consumers.ChatConsumer.as_asgi()), ]
注意:定义websocket路由时,推荐使用常见的路径前缀 (如/ws) 来区分 WebSocket 连接与普通 HTTP 连接, 因为它将使生产环境中部署 Channels 更容易,比如nginx把所有/ws的请求转给channels处理。 # myproject/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", "myproject.settings") application = ProtocolTypeRouter({ # http请求使用这个 "http": get_asgi_application(), # websocket请求使用这个 "websocket": AuthMiddlewareStack( URLRouter( chat.routing.websocket_urlpatterns ) ), })
在这里,channels的ProtocolTypeRouter会根据请求协议的类型来转发请求。AuthMiddlewareStack将使用对当前经过身份验证的用户的引用来填充连接的scope, 类似于 Django 的request对象,我们后面还会讲到。 import json from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer import datetime class ChatConsumer(WebsocketConsumer): # websocket建立连接时执行方法 def connect(self): # 从url里获取聊天室名字,为每个房间建立一个频道组 self.room_name = self.scope['url_route']['kwargs']['room_name'] self.room_group_name = 'chat_%s' % self.room_name # 将当前频道加入频道组 async_to_sync(self.channel_layer.group_add)( self.room_group_name, self.channel_name ) # 接受所有websocket请求 self.accept() # websocket断开时执行方法 def disconnect(self, close_code): async_to_sync(self.channel_layer.group_discard)( self.room_group_name, self.channel_name ) # 从websocket接收到消息时执行函数 def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json['message'] # 发送消息到频道组,频道组调用chat_message方法 async_to_sync(self.channel_layer.group_send)( self.room_group_name, { 'type': 'chat_message', 'message': message } ) # 从频道组接收到消息后执行方法 def chat_message(self, event): message = event['message'] datetime_str = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') # 通过websocket发送消息到客户端 self.send(text_data=json.dumps({ 'message': f'{datetime_str}:{message}' }))
每个自定义的Consumer类一般继承同步的WebsocketConsumer类或异步的AysncWebSocketConsumer类,它自带 self.channel_name 和self.channel_layer 属性。前者是独一无二的长连接频道名,后者提供了 send(), group_send()和group_add() 3种方法, 可以给单个频道或一个频道组发信息,还可以将一个频道加入到组。 python manage.py makemigrations python manage.py migrate python manage.py runserver
小结关键词:实战,打造