Django使用channels + websocket打造在线聊天室


Posted in Python onMay 20, 2021

Channels是Django团队研发的一个给Django提供websocket支持的框架,它同时支持http和websocket多种协议。使用channels可以让你的Django应用拥有实时通讯和给用户主动推送信息的功能。

演示效果如下所示:

Django使用channels + websocket打造在线聊天室

什么是websocket?

WebSocket 是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通讯的协议。WebSocket 使得客户端和服务器之间的数据交换变得更加简单,允许服务端主动向客户端推送数据。在 WebSocket API 中,浏览器和服务器只需要完成一次握手,两者之间就直接可以创建持久性的连接,并进行双向数据传输。

很多网站为了实现推送技术,所用的技术都是 Ajax 轮询。轮询是在特定的的时间间隔(如每1秒),由浏览器对服务器发出HTTP请求,然后由服务器返回最新的数据给客户端的浏览器。这种传统的模式带来很明显的缺点,即浏览器需要不断的向服务器发出请求,然而HTTP请求可能包含较长的头部,其中真正有效的数据可能只是很小的一部分,显然这样会浪费很多的带宽等资源。Websocket能更好的节省服务器资源和带宽,并且能够更实时地进行通讯,早已成为一种非常流行必须掌握的技术。

第一步 准备工作

首先在虚拟环境中安装django和channels(本项目使用了最新版本,均为3.X版本), 新建一个名为myproject的项目,新建一个app名为chat。如果windows下安装报错,如何解决自己网上去找吧。

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),
 ]

第二步 编写聊天室页面

我们需要利用django普通视图函数编写两个页面,一个用于展示首页(index), 通过表单让用户输入聊天室的名称(room_name),然后跳转到相应聊天室页面;一个页面用于实时展示聊天信息记录,并允许用户发送信息。

这两个页面对应的路由及视图函数如下所示:

 # 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消息至关重要。

   <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>

此时如果你使用python manage.py runserver命令启动测试服务器,当你访问一个名为/hello/的房间时,你将看到如下页面:

Django使用channels + websocket打造在线聊天室

到这里你看不到任何聊天记录,也不能发送任何消息,因为我们还没有在后端编写任何代码用于处理前端发来的消息,并返回数据。在终端你还会看到如下报错,  说Django只能处理http连接,不能处理websocket。

Django使用channels + websocket打造在线聊天室

到目前为止,我们所写的就是一个普通的django应用,还没有用到channels库处理websocket请求。接下来我们就要正式开始使用channels了。

第三步 编写后台websocket路由及处理方法

当 Django 接受 HTTP 请求时, 它会根据根 URLconf 以查找视图函数, 然后调用视图函数来处理请求。同样, 当 channels 接受 WebSocket 连接时, 它也会根据根路由配置去查找相应的处理方法。只不过channels的路由不在urls.py中配置,处理方法也不写在views.py。在channels中,这两个文件分别变成了routing.py和consumers.py。这样的好处是不用和django的常规应用混在一起。

  • routing.py:websocket路由文件,相当于django的urls.py。它根据websocket请求的url地址触发consumers.py里定义的方法。
  • consumers.py:相当于django的视图views.py,负责处理通过websocket路由转发过来的请求和数据。

在chat应用下新建routing.py, 添加如下代码。它的作用是将发送至ws/chat/<room_name>/的websocket请求转由ChatConsumer处理。

 # 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处理。

与Django类似,我们还需要把这个app的websocket路由加入到项目的根路由中去。编辑myproject/asgi.py, 添加如下代码:

 # 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对象,我们后面还会讲到。

接下来在chat应用下新建consumers.py, 添加如下代码:

 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种方法, 可以给单个频道或一个频道组发信息,还可以将一个频道加入到组。

每个频道(channel)都有一个名字。拥有频道名称的任何人都可以向频道发送消息。

一个组(group)有一个名字。具有组名称的任何人都可以按名称向组添加/删除频道,并向组中的所有频道发送消息。

注意:虽然异步Consumer类性能更优,channels推荐使用同步consumer类 , 尤其是调用Django ORM或其他同步程序时,以保持整个consumer在单个线程中并避免ORM查询阻塞整个event。调用channel_layer提供的方法时需要用async_to_sync转换一下。

除此以外,我们还使用了self.scope['url_route']['kwargs']['room_name']从路由中获取了聊天室的房间名,在channels程序中,scope是个很重要的对象,类似于django的request对象,它代表了当前websocket连接的所有信息。你可以通过scope['user']获取当前用户对象,还可以通过scope['path']获取当前当前请求路径。

第四步 运行看效果

如果不出意外,你现在的项目布局应该如下所示:

Django使用channels + websocket打造在线聊天室

连续运行如下命令,就可以看到我们文初的效果啦。

 python manage.py makemigrations

 python manage.py migrate

 python manage.py runserver

小结

我们已经使用django + channels 写了个在线聊天小应用了,现在来总结下我们所学的知识吧。

  • websocket属于全双工通讯的协议,可以在服务器和客户端之间保持长连接,实现双向数据传输。
  • 前端创建websocket对象后可以通过onmessage监听并处理后端返回的数据,可以通过send方法向后端发送数据。
  • channels对应websocket的路由和处理方法分别写在routing.py和consumers.py文件里,相当于django的urls.py和views.py。
  • 每个频道(channel)都有一个名字,拥有频道名称的任何人都可以向频道发送消息。一个组(group)有一个名字,可以包含多个频道。
  • 每个自定义的Consumer类自带 self.channel_name 和self.channel_layer 属性。前者是独一无二的频道名,后者提供了 send(), group_send()和group_add() 3种方法。
  • 在channels程序中,scope是个很重要的对象,类似于django的request对象,它代表了当前websocket连接的所有信息,比如scope['user'], scope['path']。

本文的知识你学会了吗? 学到了就点个赞吧!下期我们将利用channels + celery + redis打造个聊天机器人,欢迎关注!

以上就是Django使用channels + websocket打造在线聊天室的详细内容,更多关于Django 在线聊天室的资料请关注三水点靠木其它相关文章!

Python 相关文章推荐
python获取远程图片大小和尺寸的方法
Mar 26 Python
在Python的Django框架中获取单个对象数据的简单方法
Jul 17 Python
基于DataFrame改变列类型的方法
Jul 25 Python
python使用BeautifulSoup与正则表达式爬取时光网不同地区top100电影并对比
Apr 15 Python
python实现在函数中修改变量值的方法
Jul 16 Python
Python中print函数简单使用总结
Aug 05 Python
大家都说好用的Python命令行库click的使用
Nov 07 Python
Python socket连接中的粘包、精确传输问题实例分析
Mar 24 Python
Python任务调度模块APScheduler使用
Apr 15 Python
pytorch 查看cuda 版本方式
Jun 23 Python
Python使用socket模块实现简单tcp通信
Aug 18 Python
python定义具名元组实例操作
Feb 28 Python
教你怎么用python爬取爱奇艺热门电影
Pytorch使用shuffle打乱数据的操作
May 20 #Python
教你利用Selenium+python自动化来解决pip使用异常
python 提取html文本的方法
May 20 #Python
学会用Python实现滑雪小游戏,再也不用去北海道啦
pytorch 带batch的tensor类型图像显示操作
pytorch 中nn.Dropout的使用说明
May 20 #Python
You might like
无数据库的详细域名查询程序PHP版(2)
2006/10/09 PHP
关于使用runtimeStyle属性问题讨论文章
2007/03/08 Javascript
JS获取页面窗口大小的代码解读
2011/12/01 Javascript
深入理解JavaScript系列(14) 作用域链介绍(Scope Chain)
2012/04/12 Javascript
用jQuery获取IE9下拉框默认值问题探讨
2013/07/22 Javascript
JavaScript获取并更改input标签name属性的方法
2015/07/02 Javascript
JS模仿编辑器实时改变文本框宽度和高度大小的方法
2015/08/17 Javascript
JavaScript中的this,call,apply使用及区别详解
2016/01/29 Javascript
JavaScript 动态三角函数实例详解
2017/01/08 Javascript
通过命令行创建vue项目的方法
2017/07/20 Javascript
理解nodejs的stream和pipe机制的原理和实现
2017/08/12 NodeJs
seajs中最常用的7个功能、配置示例
2017/10/10 Javascript
jQuery实现的两种简单弹窗效果示例
2018/04/18 jQuery
Angular 利用路由跳转到指定页面的指定位置方法
2018/08/31 Javascript
js操作table中tr的顺序实现上移下移一行的效果
2018/11/22 Javascript
在vue项目中引入vue-beauty操作方法
2019/02/11 Javascript
vue项目引入ts步骤(小结)
2019/10/31 Javascript
Javascript实现鼠标移入方向感知
2020/06/24 Javascript
python持久性管理pickle模块详细介绍
2015/02/18 Python
用Python实现一个简单的线程池
2015/04/07 Python
python逆序打印各位数字的方法
2018/06/25 Python
[原创]Python入门教程5. 字典基本操作【定义、运算、常用函数】
2018/11/01 Python
浅谈PYTHON 关于文件的操作
2019/03/19 Python
python增加图像对比度的方法
2019/07/12 Python
Python 处理文件的几种方式
2019/08/23 Python
Django+uni-app实现数据通信中的请求跨域的示例代码
2019/10/12 Python
Django配置文件代码说明
2019/12/04 Python
深入理解Python 多线程
2020/06/16 Python
详解Html5中video标签那些属性和方法
2019/07/01 HTML / CSS
英国汽车座椅和婴儿车购物网站:Uber Kids
2017/04/19 全球购物
出纳岗位职责
2013/11/09 职场文书
2014年入党积极分子学习三中全会思想汇报
2014/09/13 职场文书
初中优秀学生评语
2014/12/29 职场文书
千手观音观后感
2015/06/03 职场文书
Mysql 数据库中的 redo log 和 binlog 写入策略
2022/04/26 MySQL
html中相对位置与绝对位置的具体使用
2022/05/15 HTML / CSS