Django之websocket
一:前言
很多传统的网页技术,为了实现推送功能,都是采用轮询技术 例如:弹幕啊,网页版的微信群聊啊
原理:
每次浏览器在一定的时间(例如5s)主动向客户端进行数据的请求,服务端接受客户端的请求,向浏览器响应数据
缺点:
大量的数据请求对服务器压力比较大
会有较大的延迟,例如5s中请求刚好过去 然后又产生新的数据,会有5s的延迟
长轮询
在上述轮询中显然有很明显的缺点我们需要进行解决
原理:
服务器给每个用户创建单独的队列,每个用户通过Ajax进行数据的请求,去自己各自队列中获取数据
如果没有数据响应其不会进行堵塞,而是设置最大的超时时间,在超时时间之内如果依旧没有数据返回,其会让客户端再次进行数据的请求
优点:
基本没有数据的延迟
不会有太多的请求
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
""" 1.首页自定义用户唯一表示,给每个用户初始化一个队列 2.发送按钮绑定点击事件 后端讲数据放入每一个队列中 3.书写自动获取数据的ajax代码 循环调用 4.前端获取数据DOM操作渲染页面 """ $('#d1').click(function () { $.ajax({ url:'/send_msg/', type:'post', data:{'content':$('#d2').val()}, dataType:'JSON', success:function (args) { } }) }); function getMsg(){ $.ajax({ url:'/get_msg/', type:'get', data:{'name':'{{ name }}'}, // 只要当前登陆人的队列中的数据 {#dataType:'JSON',#} success:function (args) { // 针对返回的消息做相应的处理 if(args.status){ // 有消息则渲染页面 讲消息全局放到聊天纪录里面 // 1 创建标签 var pEle = $('<p>'); // 2 给标签设置文本内容 pEle.text(args.msg); // 3 讲创建好的标签添加到聊天记录div标签内 $('#content').append(pEle) }else{ // 没有消息 则继续发送 } getMsg() // 循环请求数据 } }) } $(function () { getMsg() // 等待页面加载完毕自动执行 })
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
# 后端 # 全局大字典 q_dict = {} # {'唯一表示':队列,....} def ab_bl(request): # 获取我们自定义的客户端唯一标识 name = request.GET.get('name') # 给每一个客户端创建一个队列 q_dict[name] = queue.Queue() return render(request,'ab_bl.html',locals()) def send_msg(request): if request.method == 'POST': # 获取用户发送的消息 content = request.POST.get('content') # 讲该消息传递给所有的队列 for q in q_dict.values(): q.put(content) return HttpResponse('OK') def get_msg(request): name = request.GET.get('name') # 拿到对应的队列 q = q_dict.get(name) # 讲队列中可能有的数据取出并返回给前端浏览器 # 定义一个字典与ajax进行交互 back_dic = {'status':True,'msg':''} try: data = q.get(timeout=10) # 等10s 没有则直接报错 back_dic['msg'] = data except queue.Empty as e: back_dic['status'] = False return JsonResponse(back_dic) # return HttpResponse(json.dumps(back_dic))
二:Websocket
简介
真正做到服务端主动向客户端发送数据
一旦握手环境完毕,建立双向通道之后,客户端与服务端都可以向对方永久性的发送数据
原理
握手环节:
验证服务端是否支持websocket协议
首先客户端会产生一个随机的字符串自己本地保留一份向服务端发送一份
服务端与客户端都会将上述的随机字符串与magic string进行拼接
服务端与客户端将上述拼接好的字符串进行加密处理
服务端将加密之后的数据发送给客户端 客户端拿自己加密之后的数据与服务端发送的数据做对比
如果两边对比成功则说明握手通过支持websocket的协议
收发数据
首先会读取数据第二个字节的后七个Bit'位生成(payload),即范围大小在0-128
当数据大小等于127:其会在往后读取八个字节数的数据
当数据大小等于126:其会在往后读取两个字节的数据
当数据大小小于等于125:其不会在往后读取数据
基于上述操作之后,继续往后读取四个字节获取到一个(masking-key)
通过masking-key基于解密公式获取真实的数据
三:channels
Django默认不支持websocket的协议需要使用channels模块
安装
pip3 install channels==2.3 # 推荐使用该版本
settings配置文件
INSTALLED_APPS = [ # 1.需要先注册channels 'channels' ] ASGI_APPLICATION = 'day_webscoket.routing.application' #指定ASGI的路由 '项目名下.routing.py.文件内的变量名application'
routing.py
from channels.routing import ProtocolTypeRouter,URLRouter from django.conf.urls import url from app01 import consumers application = ProtocolTypeRouter({ 'websocket':URLRouter([ url(r'^chat/',consumers.ChatConsumer) # 路由与视图关系 ]) })
配置完成之后django由原来默认的wsgiref启动变成asgi启动
此时Django同时支持HTTP协议也支持Websocket
正常的http协议还是按照之前的写法 在urls中写路由与视图函数对应关系
而针对websocket协议则在当前文件内书写路由与视图函数对应关系
consumer
# 该文件内是专门用来写处理websocket请求的视图函数 from channels.generic.websocket import WebsocketConsumer class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): """ 客户端发来链接请求之后就会自动触发 """ def websocket_receive(self, message): """ 客户端向服务端发送消息就会自动触发 """ def websocket_disconnect(self, message): """ 客户端主动断开链接之后自动触发 """
完整代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>Title</title> <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> <script src="https://cn.vuejs.org/js/vue.min.js"></script> </head> <body> <h1>聊天室</h1> <div> <input type="text" id="d1" name="content"> <input type="button" value="发送" onclick="sendMsg()"> <input type="button" value="断开链接" onclick="closeLink()"> </div> <h1>聊天纪录</h1> <script> // 验证服务端是否支持websocket var ws = new WebSocket('ws://127.0.0.1:8000/chat/'); console.log(ws); // 3 链接成功之后自动触发 ws.onopen = function () { alert('链接成功之后自动触发:验证成功') }; // 1 给服务端发送消息 function sendMsg() { ws.send($('#d1').val()) // 将用户输入的内容发送给后端 } // 2 一旦服务端有消息 会自动触发 ws.onmessage = function (event) { // event是数据对象 真正的数据在data属性内 {#alert(event.data); // 服务端返回的真实数据#} // 将消息渲染到html页面上 var pEle = $('<p>'); $('#content').append(pEle); pEle.text(event.data); }; // 3 断开链接 function closeLink() { ws.close() } </script> </body> </html>
# 该文件内是专门用来写处理websocket请求的视图函数 from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer consumer_object_list = [] class ChatConsumer(WebsocketConsumer): def websocket_connect(self, message): """ 客户端发来链接请求之后就会自动触发 self:连接对象 message: """ # print(self) # <app01.consumers.ChatConsumer object at 0x000000000DC06828> self.accept() # 进行握手环节 发送加密字符串 # print(message) # {'type': 'websocket.connect'} # 链接成功 我就将当前对象放入全局的列表中 consumer_object_list.append(self) def websocket_receive(self, message): """ 客户端向服务端发送消息就会自动触发 """ # print(message) # {'type': 'websocket.receive', 'text': 'asdasdasdasd'} msg = message.get('text') # 获取前端传来的内容 print(msg) for obj in consumer_object_list: obj.send(msg) # 返回给所有的连接对象 实现群聊 def websocket_disconnect(self, message): """ 客户端主动断开链接之后自动触发 """ print('断开链接了') # 服务端断开链接 就去列表中删除对应的客户端对象 consumer_object_list.remove(self) raise StopConsumer
四: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
使用
官方推荐使用redis作为channel layer,所以先安装channels_redis
安装
pip install channels_redis==2.3.3
修改缓存
CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('127.0.0.1', 6379)], }, }, }
案例
import channels.layers from asgiref.sync import async_to_sync channel_layer=channels.layers.get_channel_layer() async_to_sync(channel_layer.send)('test',{'site':"www"}) # 发送消息 async_to_sync(channel_layer.receive)('test') # 接受消息