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实现的简单文件传输服务器和客户端
Apr 08 Python
在Python中操作列表之List.pop()方法的使用
May 21 Python
Python线程指南详细介绍
Jan 05 Python
python操作MySQL 模拟简单银行转账操作
Sep 27 Python
使用Python获取并处理IP的类型及格式方法
Nov 01 Python
详解基于python-django框架的支付宝支付案例
Sep 23 Python
把vgg-face.mat权重迁移到pytorch模型示例
Dec 27 Python
Jupyter 无法下载文件夹如何实现曲线救国
Apr 22 Python
python 解决mysql where in 对列表(list,,array)问题
Jun 06 Python
Keras设置以及获取权重的实现
Jun 19 Python
简述python Scrapy框架
Aug 17 Python
python Polars库的使用简介
Apr 21 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
多人战的战术与战略
2020/03/04 星际争霸
46 个非常有用的 PHP 代码片段
2016/02/16 PHP
一个简单的js渐显(fadeIn)渐隐(fadeOut)类
2010/06/19 Javascript
使用javascript做的一个随机点名程序
2014/02/13 Javascript
jQuery实现简单的间隔向上滚动效果
2015/03/09 Javascript
AngularJS学习笔记之基本指令(init、repeat)
2015/06/16 Javascript
jQuery中$.grep() 过滤函数 数组过滤
2016/11/22 Javascript
前端js弹出框组件使用方法
2020/08/24 Javascript
vue实现ToDoList简单实例
2017/02/07 Javascript
微信小程序loading组件显示载入动画用法示例【附源码下载】
2017/12/09 Javascript
使用vue-router设置每个页面的title方法
2018/02/11 Javascript
vue数据控制视图源码解析
2018/03/28 Javascript
nodeJS进程管理器pm2的使用
2019/01/09 NodeJs
Vue中Axios从远程/后台读取数据
2019/01/21 Javascript
vue路由守卫,限制前端页面访问权限的例子
2019/11/11 Javascript
vue动画—通过钩子函数实现半场动画操作
2020/08/09 Javascript
详解nginx配置vue h5 history去除#号
2020/11/09 Javascript
[02:51]2014DOTA2 TI小组赛总结中国军团全部进军钥匙球馆
2014/07/15 DOTA
使用python编写批量卸载手机中安装的android应用脚本
2014/07/21 Python
微信 用脚本查看是否被微信好友删除
2016/10/28 Python
python 筛选数据集中列中value长度大于20的数据集方法
2018/06/14 Python
Python爬虫之pandas基本安装与使用方法示例
2018/08/08 Python
matplotlib自定义鼠标光标坐标格式的实现
2021/01/08 Python
高中生毕业自我鉴定
2013/10/10 职场文书
女大学生自我鉴定
2013/12/09 职场文书
运动会广播稿100字
2014/01/11 职场文书
后勤部经理岗位职责
2014/02/23 职场文书
高等教育专业自荐信范文
2014/03/26 职场文书
文明工地标语
2014/06/16 职场文书
和谐社区口号
2014/06/19 职场文书
社保委托书怎么写
2014/08/02 职场文书
2014年驾驶员工作总结
2014/11/18 职场文书
2014年加油站工作总结
2014/12/04 职场文书
2014小学一年级班主任工作总结
2014/12/05 职场文书
老乡聚会通知
2015/04/23 职场文书
Django基础CBV装饰器和中间件
2022/03/22 Python