服务端和客户端通信的三种方式
轮询
客户端每隔一段时间向服务器发送ajax请求,看是否有新的消息
缺点
延迟为时间间隔,请求次数多
长轮询
服务端给每一个客户端建立队列,如果客户端发起ajax请求,就会去各自对应的队列中去获取数据,如果没有数据就阻塞,但是不会一直阻塞,可以设置阻塞的时间timeout,比如阻塞30s后返回响应,触发客户端的回调函数,客户端再次发起ajax请求
相对于轮询
消息基本没有延迟
请求减少
兼容性好(web的qq和微信都是基于长轮询的)
websocket
注意
websocket是一种加密的网络协议
协议原理
主要分为两阶段:握手阶段和收发数据阶段
#握手环节(handshake)
目的:验证服务端是否支持websocket协议
客户端浏览器第一次访问服务端的时候
浏览器内部会自动生成一个随机字符串,将该随机字符串发送给服务端(基于http协议),自己也保留一份(请求头里面)
服务端接受到随机字符串之后,会让它跟magic string(全球统一)做字符串的拼接
然后利用加密算法对拼接好的字符串做加密处理(sha1/base64)
客户端也在对产生的随机字符串做上述的拼接和加密操作
服务单将产生好的随机字符串发送给客户端浏览器(响应头里面)
客户端浏览器会比对服务单发送的随机字符串很我浏览器本地操作完的随机字符串是否一致,如果一致说明该服务端支持websocket,如果不一致则不支持
#收发数据(send/onmessage)
验证成功之后就可以数据交互了 但是交互的数据是加密的 需要解密处理
前提:
1.数据基于网络传输都是二进制格式
2.单位换算 8bit = 1bytes
读取第二个字节的后七位称之为payload
1.根据payload大小决定不同的处理方式
=127 再读取8个字节 作为数据报
=126 再读取2个字节 作为数据报
<=125 不再往后读了
2.步骤1之后 会对剩下的数据再读取4个字节(masking-key)
之后依据masking-key算出真实数据
var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}
基于长轮询的群聊Demo
界面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
{% load staticfiles %}
<script src="{% static 'myjq.js' %}"></script>
</head>
<body>
<h1>聊天室:{{ name }}</h1>
<div>
<input type="text" name="content" id="d1">
<button id="d2">发送</button>
</div>
<h1>聊天纪录</h1>
<div class="record">
</div>
<script>
// 朝后端发送用户消息
$('#d2').click(function () {
$.ajax({
url:'/send_msg/',
type:'post',
data:{'content':$("#d1").val(),'csrfmiddlewaretoken':'{{ csrf_token }}'},
{#dataType:"JSON",#}
success:function (args) {
}
})
});
// 向后端请求数据代码
function getMsg() {
$.ajax({
url:'/get_msg/',
type:'get',
data:{'name':'{{ name }}'},
{#dataType:"JSON",#}
success:function (args) {
if(args.status){
// 有消息 应该DOM操作渲染到页面上
// 1 创建标签
var pEle = $('<p>');
// 2 给标签添加文本内容
pEle.text(args.msg);
// 3 添加到div中
$('.record').append(pEle)
}else{
// 没有则继续发送
}
getMsg()
}
})
}
// 页面加载完毕之后自动触发getMsg函数的执行,监听是否有新的消息
$(function () { // 等待页面加载完毕之后自动调用getMsg函数
getMsg()
})
</script>
</body>
</html>
views
from django.shortcuts import render,HttpResponse
import queue
# Create your views here.
#定义全局字典存储浏览器和队列的关系
q_dict = {}
def home(request):
#获取用户唯一标识,用于创建队列
name = request.GET.get('name')
#给每个刚请求界面的用户创建一个队列
q_dict[name] = queue.Queue()
return render(request,'index.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')
#长轮询核心代码
import json
from django.http import JsonResponse
def get_msg(request):
name = request.GET.get("name")
#去对应队列取数据
q = q_dict.get(name)
back_dic = {'status':True,'msg':''}
try:
data = q.get(timeout=10)
print(data)
back_dic['msg'] = data
except queue.Empty as e:
back_dic['status'] = False
return JsonResponse(back_dic)
websocket支持
django
-默认不支持
-下载第三方模块:channels
flask
-默认不支持
-下载第三方模块:geventwebsocket
tornado
-默认就是支持的
# 如果你想在django支持websocket的话 需要安装对应的模块
"""
在django安装channles时候不要直接安装最新版本 可能会自动将的解释器中的django版本改为最新版
python解释器环境推荐使用3.6(官网:3.5可能会出问题,3.7也可能会出问题,没有给出具体的解释)
"""
pip3 install channles==2.3
django使用websocket
1.setting配置
INSTALLED_APPS = [
# 1 注册channles应用
'channels'
]
2.添加配置
ASGI_APPLICATION = 'qq.routing.application'
# ASGI_APPLICATION = '项目名同名的文件夹名.项目内部的py文件名(默认就叫routing,注意不在app里面).routing文件内的变量名(默认就叫application)'
3.application配置
#在routing.py里面写下如下代码
from channels.routing import ProtocolTypeRouter,URLRouter
from django.conf.urls import url
from app01 import consumers
application =ProtocolTypeRouter({
'websocket':URLRouter([
# websocket请求路由与视图函数的对应关系
url(r'^chat/$',consumers.ChatConsumer)
])
})
总结:配置完成后django由原来默认的wsgiref替换成asgi启动(asgi内部是基于达芙妮)
上述配置完成后 ,django就会同时支持http协议和websocket协议
原先基于http协议的路由与视图函数对应关系还是跟之前一样urls.py、views.py
针对websocket协议的路由与视图函数对应关系则需要在routing.py和consumers.py(在对应的应用下创建即可)
业务逻辑代码consumers
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
"""
客户端请求链接之后自动触发
:param message: 消息数据
"""
self.accept() # 建立链接
def websocket_receive(self, message):
"""
客户端浏览器发送消息来的时候自动触发
:param message: 消息数据 {'type': 'websocket.receive', 'text': 'hello'}
"""
text = message.get('text')
print(text)
def websocket_disconnect(self, message):
"""
客户端断开链接之后自动触发
:param message:
"""
raise StopConsumer() # 主动报异常自动断开链接 无需做处理 内部自动捕获
js
<h1>聊天室</h1>
<div>
<input type="text" id="text" placeholder="请输入">
<input type="button" value="发送" onclick="sendMsg()">
<input type="button" value="断开链接" onclick="close()">
</div>
<h1>聊天纪录</h1>
<div id="content">
</div>
<script>
var ws = new WebSocket('ws://127.0.0.1:8000/chat/');
// 1 发送消息 ws.send()
// 2 握手成功之后 自动触发 ws.onopen
// 3 服务端发送消息过来 自动触发 ws.onmessage
// 4 断开链接 ws.close()
// 0 握手成功之后自动触发
ws.onopen = function () {
alert('建立成功')
};
// 1 给服务端发送消息
function sendMsg() {
ws.send($('#text').val());
}
// 2 接受服务端发送过来的消息
ws.onmessage = function (event) { // event是对象
var dataValue = event.data; // 获取服务端的数据
// 将数据动态渲染到页面上
var pEle = $('<p>');
pEle.text(dataValue);
$('#content').append(pEle)
};
// 3 断开链接
function close() {
ws.close()
}
</script>