轮询
原理: 利用Ajax定时朝后端发送请求,比如每隔五秒钟发一次请求,那么你的数据延迟就可能会高达五秒
特点: 数据延迟,消耗资源过大,请求次数太多
长轮询
原理: 利用Ajax + 队列 定时朝后端发送请求, 如果没有数据则会阻塞但是不会一直阻塞, 比如阻塞你30秒,还没有数据则返回,然后让客户端浏览器再次发送请求数据的请求
特点: 相对于轮询基本是没有消息延迟的,请求次数降低了很多
Websocket
WebSocket 的最大特点就是,浏览器与服务端建立链接之后默认不再断开,服务器可以主动向客户端推送信息,客户端也可以主动向服务器发送信息
原理
- 握手环节:主要验证服务端是否支持websocket通信
第一次访问服务端的时候(基于http协议) 浏览器会自动生成一个随机字符串放在请求头中带给服务端然后自己也保留一份
Sec-WebSocket-Key: ePW8kp1XqLNWbJxE/Q38SA==
服务端和客户端都对随机字符串做下面的操作:
1.随机字符串 + magic string拼接
2.然后再将拼接好的结果进行加密处理(sha1/base64)的到密文
服务端将产生的密文通过响应头再次发送给客户端浏览器进行对比,假设比对上了 建立websocket链接 基于该链接收发消息
- 收发数据
由于websocket是加密传输数据的,基于网络传输的数据都是二进制格式 对应到我们python中就是bytes类型
进行如下解密:
1.先读取第2个字节后7位(payload) 针对后七位的位数做不同的处理机制
=127:再往后读取8个字节
=126:再往后读取2个字节
<=125:不再往后读取
基于上述操作之后 所有的情况都继续往后读取4个字节(masking-key),基于该masking-key依据解密公式解密出真实数据
补充:
前端关键代码
<script>
var ws = new WebSocket('ws://127.0.0.1:22/')
</script>
// 上述代码做的事
// 1.自动生产随机字符串并发送给服务端
// 2.自动对随机字符串继续一系列操作 magic string 加密
// 3.自动对比服务端加密字符串、
<!--通过ws对象点send方法即可实现websocket的数据交互-->
Django实现Websocket
不是所有的服务端都支持Websocket,
Django 默认不支持,只支持HTTP协议
Flask 默认不支持
Tronado 默认支持
channles
在django中如果想要基于websocket开发项目 你需要安装模块:channles
安装
pip3 install channels==2.3
注意:
版本不要使用最新的,如果安装最新的可能会自动把你的django版本升级到最新版
对应的解释器环境建议使用3.6
channels模块内部已经帮我们封装好了 握手/加密/解密的工作
使用
1.需要在配置文件settings.py中注册channles应用
INSTALLED_APPS = [
...
# 1.需要先注册channels
'channels'
]
2.还需要在配置文件settings.py中配置以下参数
ASGI_APPLICATION = 'channels_demo.routing.application'
# '与项目名同名的文件夹名.routing文件名.文件内的变量名application'
# 这里的channels_demo是项目名
3.在项目文件夹下创建routing.py文件,文件内写一下内容
from channels.routing import ProtocolTypeRouter,URLRouter
application = ProtocolTypeRouter({
'websocket':URLRouter([
# 路由与视图函数对应关系
url(r'^chat/',consumers.ChatConsumer)
# consumers是处理Websocket请求的视图文件
# ChatConsumer是视图类
])
})
上述配置完成后,启动django项目会发现
django由原来默认的wsgiref启动变成asgi启动
注意配置完成后,django就会即支持http协议也支持websocket协议
兼容http协议源码:
if "http" not in self.application_mapping:
self.application_mapping["http"] = AsgiHandler
注意:
正常的http协议还是按照之前的写法 在urls中写路由与视图函数对应关系
而针对websocket协议则在当前文件内书写路由与视图函数对应关系
4.在视图文件consumer.py中书写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):
"""
客户端主动断开链接之后自动触发
"""
案例
基于channels来实现我们的多人聊天室
前端代码
- index.html
<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>
<div id="content"></div>
<script>
// 验证服务端是否支持websocket
var ws = new WebSocket('ws://127.0.0.1:8000/chat/');
// 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>');
pEle.text(event.data);
$('#content').append(pEle)
};
// 3 断开链接
function closeLink() {
ws.close()
}
</script>
</body>
后端代码
- urls.py
urlpatterns = [
url(r'^admin/', admin.site.urls),
# 默认只支持http协议
url(r'^index/',views.index)
]
- views.py
def index(request):
return render(request,'index.html')
- routing.py
application = ProtocolTypeRouter({
'websocket':URLRouter([
url(r'^chat/',consumers.ChatConsumer)
])
})
- consumers.py
rom channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
consumer_object_list = []
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
"""
客户端发来链接请求之后就会自动触发
:param message:
:return:
"""
# print('验证')
self.accept() # 向服务端发送加密字符串
# self就是每一个客户端对象
# 链接成功 我就将当前对象放入全局的列表中
consumer_object_list.append(self)
def websocket_receive(self, message):
"""
客户端向服务端发送消息就会自动触发
:param message:内部包含客户端给你发送的消息 {'type': 'websocket.receive', 'text': '大宝贝'}
:return:
"""
print(message)
# 给客户端回消息
# self.send(text_data=message.get('text'))
# 给列表中所有的对象都发送消息
for obj in consumer_object_list:
obj.send(text_data=message.get('text'))
def websocket_disconnect(self, message):
"""
客户端主动断开链接之后自动触发
:param message:
:return:
"""
print('断开链接了')
# 服务端断开链接 就去列表中删除对应的客户端对象
consumer_object_list.remove(self)
raise StopConsumer
总结
前端四个方法
var ws = new WebSocket('ws://127.0.0.1:8000/chat/');
ws.onopen
ws.send()
ws.onmessage
ws.close()
后端三个方法
websocket_connect
websocket_receive
websocket_disconnect
案例:websocket + celery + redis发布订阅 实现服务端不断向web端推送数据
from asgiref.sync import async_to_sync
from channels.generic.websocket import WebsocketConsumer
class DomainEvent(WebsocketConsumer):
# websocket连接时会执行 为了实现线程高可用,上生产应使用celery+supervisor
def connect(self):
# 这里会取到url中传入的report_id
# self.report_name = self.scope['url_route']['kwargs']['report_id']
# self.report_group_name = 'chat_%s' % self.report_name
self.group_name = 'channel'
# group_add 加入一个小组
# channel_layer下面的方法属于通道层方法是异步的,因此使用async_to_sync进行包装
async_to_sync(self.channel_layer.group_add)(
self.group_name,
self.channel_name
)
# accept表示建立连接,否则可以使用close
self.accept()
# 断开websocket连接时会执行
def disconnect(self, close_code):
# 断开与小组的连接 group_discard
async_to_sync(self.channel_layer.group_discard)(
self.group_name,
self.channel_name
)
logger.info('websocket close success')
# 接收到消息会执行
def receive(self, text_data):
# text_data 接收到的消息
# recv = json.loads(text_data)
logger.info(text_data)
# group_send 向小组发送消息
# self.report_group_name 指小组的名称
# 'type': 'chat_message' 会调用chat_message发送消息
async_to_sync(self.channel_layer.group_send)(
self.group_name,
{
'type': 'push_message',
'message': text_data
}
)
# group_send被调用时,会执行指定选择的方法
def push_message(self, event):
message = event['message']
# message['msg'] = message['msg'].decode('unicode_escape')
self.send(text_data=json.dumps({
'message': message
}))
celery task:
@app.task(queue='default')
def usb_policy(devices, arg):
public = RedisHelper()
......
signal = {'event_type':'usb_policy'}
public.public(json.dumps(signal), chan='policy') # 向redis队列中发布消息
另起线程监控redis订阅队列,并将消息通过channel_layer发送web:
def WebSocketEvent(subscribes):
def push(channel, msg):
channel_layer = get_channel_layer()
async_to_sync(channel_layer.group_send)(
channel,
{"type": "push_message", "message": msg}
)
redis_sub = subscribes.subscribe(chan='policy')
for recv in redis_sub.listen():
data = json.loads(recv.get('data').decode('utf-8'))
push('channel', data)
if __name__ == '__main__':
redis_helper = RedisHelper()
websocket_channel_worker_p = Process(target=WebSocketEvent, args=(redis_helper,))
websocket_channel_worker_p.daemon = True
websocket_channel_worker_p.start()
websocket_channel_worker_p.join()