Django使用Channels实现WebSocket的方法


Posted in Python onJuly 28, 2019

WebSocket - 开启通往新世界的大门

WebSocket是什么?

WebSocket是一种在单个TCP连接上进行全双工通讯的协议。WebSocket允许服务端主动向客户端推送数据。在WebSocket协议中,客户端浏览器和服务器只需要完成一次握手就可以创建持久性的连接,并在浏览器和服务器之间进行双向的数据传输。

WebSocket有什么用?

WebSocket区别于HTTP协议的一个最为显著的特点是,WebSocket协议可以由服务端主动发起消息,对于浏览器需要及时接收数据变化的场景非常适合,例如在Django中遇到一些耗时较长的任务我们通常会使用Celery来异步执行,那么浏览器如果想要获取这个任务的执行状态,在HTTP协议中只能通过轮训的方式由浏览器不断的发送请求给服务器来获取最新状态,这样发送很多无用的请求不仅浪费资源,还不够优雅,如果使用WebSokcet来实现就很完美了

WebSocket的另外一个应用场景就是下文要说的聊天室,一个用户(浏览器)发送的消息需要实时的让其他用户(浏览器)接收,这在HTTP协议下是很难实现的,但WebSocket基于长连接加上可以主动给浏览器发消息的特性处理起来就游刃有余了

初步了解WebSocket之后,我们看看如何在Django中实现WebSocket

Channels

Django本身不支持WebSocket,但可以通过集成Channels框架来实现WebSocket

Channels是针对Django项目的一个增强框架,可以使Django不仅支持HTTP协议,还能支持WebSocket,MQTT等多种协议,同时Channels还整合了Django的auth以及session系统方便进行用户管理及认证。

我下文所有的代码实现使用以下python和Django版本

python==3.6.3
django==2.2

集成Channels

我假设你已经新建了一个django项目,项目名字就叫webapp,目录结构如下

project
 - webapp
  - __init__.py
  - settings.py
  - urls.py
  - wsgi.py
 - manage.py

1.  安装channels

pip install channels==2.1.7

2.  修改settings.py文件,

# APPS中添加channels
INSTALLED_APPS = [
 'django.contrib.staticfiles',
 'channels',
]
# 指定ASGI的路由地址
ASGI_APPLICATION = 'webapp.routing.application'

channels运行于ASGI协议上,ASGI的全名是Asynchronous Server Gateway Interface。它是区别于Django使用的WSGI协议 的一种异步服务网关接口协议,正是因为它才实现了websocket

ASGI_APPLICATION 指定主路由的位置为webapp下的routing.py文件中的application

3.  setting.py的同级目录下创建routing.py路由文件,routing.py类似于Django中的url.py指明websocket协议的路由

from channels.routing import ProtocolTypeRouter

application = ProtocolTypeRouter({
 # 暂时为空,下文填充
})

4.  运行Django项目

C:\python36\python.exe D:/demo/tailf/manage.py runserver 0.0.0.0:80
Performing system checks...
Watching for file changes with StatReloader
System check identified no issues (0 silenced).
April 12, 2019 - 17:44:52
Django version 2.2, using settings 'webapp.settings'
Starting ASGI/Channels version 2.1.7 development server at http://0.0.0.0:80/
Quit the server with CTRL-BREAK.

仔细观察上边的输出会发现Django启动中的Starting development server已经变成了Starting ASGI/Channels version 2.1.7 development server,这表明项目已经由django使用的WSGI协议转换为了Channels使用的ASGI协议

至此Django已经基本集成了Channels框架

构建聊天室

上边虽然在项目中集成了Channels,但并没有任何的应用使用它,接下来我们以聊天室的例子来讲解Channels的使用

假设你已经创建好了一个叫chat的app,并添加到了settings.py的INSTALLED_APPS中,app的目录结构大概如下

chat
 - migrations
  - __init__.py
 - __init__.py
 - admin.py
 - apps.py
 - models.py
 - tests.py
 - views.py

我们构建一个标准的Django聊天页面,相关代码如下

url:
from django.urls import path
from chat.views import chat
urlpatterns = [
 path('chat', chat, name='chat-url')
]
view:
from django.shortcuts import render
def chat(request):
 return render(request, 'chat/index.html')
template:
{% extends "base.html" %}
{% block content %}
 <textarea class="form-control" id="chat-log" disabled rows="20"></textarea><br/>
 <input class="form-control" id="chat-message-input" type="text"/><br/>
 <input class="btn btn-success btn-block" id="chat-message-submit" type="button" value="Send"/>
{% endblock %}

通过上边的代码一个简单的web聊天页面构建完成了,访问页面大概样子如下:

Django使用Channels实现WebSocket的方法

接下来我们利用Channels的WebSocket协议实现消息的发送接收功能

1.  先从路由入手,上边我们已经创建了routing.py路由文件,现在来填充里边的内容

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
application = ProtocolTypeRouter({
 'websocket': AuthMiddlewareStack(
  URLRouter(
   chat.routing.websocket_urlpatterns
  )
 ),
})

ProtocolTypeRouter: ASIG支持多种不同的协议,在这里可以指定特定协议的路由信息,我们只使用了websocket协议,这里只配置websocket即可

AuthMiddlewareStack: django的channels封装了django的auth模块,使用这个配置我们就可以在consumer中通过下边的代码获取到用户的信息

def connect(self):
 self.user = self.scope["user"]

self.scope类似于django中的request,包含了请求的type、path、header、cookie、session、user等等有用的信息

URLRouter: 指定路由文件的路径,也可以直接将路由信息写在这里,代码中配置了路由文件的路径,会去chat下的routeing.py文件中查找websocket_urlpatterns,chat/routing.py内容如下

from django.urls import path
from chat.consumers import ChatConsumer

websocket_urlpatterns = [
 path('ws/chat/', ChatConsumer),
]

routing.py路由文件跟django的url.py功能类似,语法也一样,意思就是访问ws/chat/都交给ChatConsumer处理

2.  接着编写consumer,consumer类似django中的view,内容如下

from channels.generic.websocket import WebsocketConsumer
import json
class ChatConsumer(WebsocketConsumer):
 def connect(self):
  self.accept()
 def disconnect(self, close_code):
  pass
 def receive(self, text_data):
  text_data_json = json.loads(text_data)
  message = '运维咖啡吧:' + text_data_json['message']
  self.send(text_data=json.dumps({
   'message': message
  }))

这里是个最简单的同步websocket consumer类,connect方法在连接建立时触发,disconnect在连接关闭时触发,receive方法会在收到消息后触发。整个ChatConsumer类会将所有收到的消息加上“运维咖啡吧:”的前缀发送给客户端

3.  最后我们在html模板页面添加websocket支持

{% extends "base.html" %}
{% block content %}
 <textarea class="form-control" id="chat-log" disabled rows="20"></textarea><br/>
 <input class="form-control" id="chat-message-input" type="text"/><br/>
 <input class="btn btn-success btn-block" id="chat-message-submit" type="button" value="Send"/>
{% endblock %}
{% block js %}
<script>
 var chatSocket = new WebSocket(
 'ws://' + window.location.host + '/ws/chat/');
 chatSocket.onmessage = function(e) {
 var data = JSON.parse(e.data);
 var message = data['message'];
 document.querySelector('#chat-log').value += (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) {
 var messageInputDom = document.querySelector('#chat-message-input');
 var message = messageInputDom.value;
 chatSocket.send(JSON.stringify({
  'message': message
 }));
 messageInputDom.value = '';
 };
</script>
{% endblock %}

WebSocket对象一个支持四个消息:onopen,onmessage,oncluse和onerror,我们这里用了两个onmessage和onclose

onopen: 当浏览器和websocket服务端连接成功后会触发onopen消息

onerror: 如果连接失败,或者发送、接收数据失败,或者数据处理出错都会触发onerror消息

onmessage: 当浏览器接收到websocket服务器发送过来的数据时,就会触发onmessage消息,参数e包含了服务端发送过来的数据

onclose: 当浏览器接收到websocket服务器发送过来的关闭连接请求时,会触发onclose消息

4.  完成前边的代码,一个可以聊天的websocket页面就完成了,运行项目,在浏览器中输入消息就会通过websocket-->rouging.py-->consumer.py处理后返回给前端

Django使用Channels实现WebSocket的方法

启用Channel Layer

上边的例子我们已经实现了消息的发送和接收,但既然是聊天室,肯定要支持多人同时聊天的,当我们打开多个浏览器分别输入消息后发现只有自己收到消息,其他浏览器端收不到,如何解决这个问题,让所有客户端都能一起聊天呢?

Channels引入了一个layer的概念,channel layer是一种通信系统,允许多个consumer实例之间互相通信,以及与外部Djanbo程序实现互通。

channel layer主要实现了两种概念抽象:

channel name: channel实际上就是一个发送消息的通道,每个Channel都有一个名称,每一个拥有这个名称的人都可以往Channel里边发送消息

group: 多个channel可以组成一个Group,每个Group都有一个名称,每一个拥有这个名称的人都可以往Group里添加/删除Channel,也可以往Group里发送消息,Group内的所有channel都可以收到,但是无法发送给Group内的具体某个Channel

了解了上边的概念,接下来我们利用channel layer实现真正的聊天室,能够让多个客户端发送的消息被彼此看到

1.  官方推荐使用redis作为channel layer,所以先安装channels_redis

pip install channels_redis==2.3.3

2.  然后修改settings.py添加对layer的支持

CHANNEL_LAYERS = {
 'default': {
  'BACKEND': 'channels_redis.core.RedisChannelLayer',
  'CONFIG': {
   "hosts": [('ops-coffee.cn', 6379)],
  },
 },
}

添加channel之后我们可以通过以下命令检查通道层是否能够正常工作

>python manage.py shell
Python 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
(InteractiveConsole)
>>> import channels.layers
>>> channel_layer = channels.layers.get_channel_layer()
>>>
>>> from asgiref.sync import async_to_sync
>>> async_to_sync(channel_layer.send)('test_channel',{'site':'https://ops-coffee.cn'})
>>> async_to_sync(channel_layer.receive)('test_channel')
{'site': 'https://ops-coffee.cn'}
>>>

3.  consumer做如下修改引入channel layer

from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
import json
class ChatConsumer(WebsocketConsumer):
 def connect(self):
  self.room_group_name = 'ops_coffee'
  # 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 = text_data_json['message']
  # Send message to room group
  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
  self.send(text_data=json.dumps({
   'message': message
  }))

这里我们设置了一个固定的房间名作为Group name,所有的消息都会发送到这个Group里边,当然你也可以通过参数的方式将房间名传进来作为Group name,从而建立多个Group,这样可以实现仅同房间内的消息互通

当我们启用了channel layer之后,所有与consumer之间的通信将会变成异步的,所以必须使用async_to_sync

一个链接(channel)创建时,通过group_add将channel添加到Group中,链接关闭通过group_discard将channel从Group中剔除,收到消息时可以调用group_send方法将消息发送到Group,这个Group内所有的channel都可以收的到

group_send中的type指定了消息处理的函数,这里会将消息转给chat_message函数去处理

4.  经过以上的修改,我们再次在多个浏览器上打开聊天页面输入消息,发现彼此已经能够看到了,至此一个完整的聊天室已经基本完成

修改为异步

我们前边实现的consumer是同步的,为了能有更好的性能,官方支持异步的写法,只需要修改consumer.py即可

from channels.generic.websocket import AsyncWebsocketConsumer
import json
class ChatConsumer(AsyncWebsocketConsumer):
 async def connect(self):
  self.room_group_name = 'ops_coffee'
  # 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
  }))

其实异步的代码跟之前的差别不大,只有几个小区别:

ChatConsumer由WebsocketConsumer修改为了AsyncWebsocketConsumer

所有的方法都修改为了异步defasync def

用await来实现异步I/O的调用

channel layer也不再需要使用async_to_sync了

好了,现在一个完全异步且功能完整的聊天室已经构建完成了

代码地址

我已经将以上的演示代码上传至Github方便你在实现的过程中查看参考,具体地址为:

https://github.com/ops-coffee/demo/tree/master/websocket

总结

以上所述是小编给大家介绍的Django使用Channels实现WebSocket的方法,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对三水点靠木网站的支持!
如果你觉得本文对你有帮助,欢迎转载,烦请注明出处,谢谢!

Python 相关文章推荐
Python创建日历实例
Aug 21 Python
用Python实现一个简单的线程池
Apr 07 Python
python3中dict(字典)的使用方法示例
Mar 22 Python
pytorch中tensor的合并与截取方法
Jul 26 Python
利用arcgis的python读取要素的X,Y方法
Dec 22 Python
Python之使用adb shell命令启动应用的方法详解
Jan 07 Python
Python3解释器知识点总结
Feb 19 Python
django框架使用orm实现批量更新数据的方法
Jun 21 Python
对python中GUI,Label和Button的实例详解
Jun 27 Python
pyqt5 QScrollArea设置在自定义侧(任何位置)
Sep 25 Python
python中的socket实现ftp客户端和服务器收发文件及md5加密文件
Apr 01 Python
python工具——Mimesis的简单使用教程
Jan 16 Python
Django实现web端tailf日志文件功能及实例详解
Jul 28 #Python
Python assert语句的简单使用示例
Jul 28 #Python
对Django中static(静态)文件详解以及{% static %}标签的使用方法
Jul 28 #Python
解决Django Static内容不能加载显示的问题
Jul 28 #Python
基于Django静态资源部署404的解决方法
Jul 28 #Python
Python占用的内存优化教程
Jul 28 #Python
解决Django加载静态资源失败的问题
Jul 28 #Python
You might like
跟我学Laravel之配置Laravel
2014/10/15 PHP
PHP处理大量表单字段的便捷方法
2015/02/07 PHP
php自动载入类用法实例分析
2016/06/24 PHP
PHP call_user_func和call_user_func_array函数的简单理解与应用分析
2019/11/25 PHP
深入理解javascript构造函数和原型对象
2014/09/23 Javascript
浅析创建javascript对象的方法
2016/05/13 Javascript
数组Array的一些方法(总结)
2017/02/17 Javascript
Vue.js中用webpack合并打包多个组件并实现按需加载
2017/02/17 Javascript
详解angularjs实现echart图表效果最简洁教程
2017/11/29 Javascript
Vue axios全局拦截 get请求、post请求、配置请求的实例代码
2018/11/28 Javascript
推荐15个最好用的JavaScript代码压缩工具
2019/02/13 Javascript
LayUI数据接口返回实体封装的例子
2019/09/12 Javascript
windows实现npm和cnpm安装步骤
2019/10/24 Javascript
ES6学习笔记之let与const用法实例分析
2020/01/22 Javascript
js验证密码强度解析
2020/03/18 Javascript
Vue中keep-alive组件的深入理解
2020/08/23 Javascript
vue中配置scss全局变量的步骤
2020/12/28 Vue.js
[36:54]Mineski vs Winstrike 2018国际邀请赛小组赛BO2 第一场 8.16
2018/08/17 DOTA
[51:07]VGJ.S vs Pain 2018国际邀请赛小组赛BO2 第一场 8.17
2018/08/20 DOTA
Python中使用SAX解析xml实例
2014/11/21 Python
python执行外部程序的常用方法小结
2015/03/21 Python
Windows上使用virtualenv搭建Python+Flask开发环境
2016/06/07 Python
Python正则捕获操作示例
2017/08/19 Python
Python操作MySQL数据库的两种方式实例分析【pymysql和pandas】
2019/03/18 Python
Python中单线程、多线程和多进程的效率对比实验实例
2019/05/14 Python
python批量修改ssh密码的实现
2019/08/08 Python
Python 使用Opencv实现目标检测与识别的示例代码
2020/09/08 Python
如何让IE9以下版本(ie6/7/8)认识html5元素
2013/04/01 HTML / CSS
ZWILLING双立人法国网上商店:德国刀具锅具厨具品牌
2019/08/28 全球购物
Java如何获得ResultSet的总行数
2016/09/03 面试题
关于护士节的演讲稿
2014/05/26 职场文书
企业安全生产目标责任书
2014/07/23 职场文书
普通党员对照检查材料
2014/09/24 职场文书
银行会计主管岗位职责
2014/10/01 职场文书
运动会广播稿50字-100字
2014/10/11 职场文书
大学生团日活动总结
2015/05/06 职场文书