websocket实现服务端主动给客户端推送消息
websocket实现服务端主动给客户端推送消息
HTTP协议中的四大特性中有无链接:一次请求一次响应后断开链接,因此基于HTTP协议实现服务端改客户端主动推送消息有点麻烦
1. 服务端给客户端主动推送消息有以下应用情景:
-
-
任务的执行流程
-
群聊功能
-
2. 如何实现服务端给客户端主动推送消息
大的方向有两种方式:
1. 伪实现
让客户端浏览器每隔一段时间偷偷的去服务器请求数据,这样能实现效果,但是内部本质还是客户端朝服务端发送消息
-
""" 让浏览器定时(例如每隔5秒发一次)通过ajax朝服务端发送请求获取数据 缺点: 消息延迟严重 请求次数多 消耗资源过大 """
-
""" 服务端给每个浏览器创建一个队列,让浏览器通过ajax向后端偷偷的发送请求,去各自对应的队列中获取数据,如果没有数据则会有阻塞,
但是不会一直阻塞,比如最多阻塞30秒(pending)后给一个响应,无论响应是否是真正的数据,都会再次通过回调函数调用请求数据的代码 优点: 消息基本没有延迟 请求次数降低 消耗资源减少 """ # 大公司需要考虑兼容性问题 追求兼容 目前网页版本的微信和qq用的就是长轮询
基于ajax、队列、异常处理实现长轮询简易版群聊功能
1. ajax操作:异步提交,局部刷新,可以使用他来向服务端偷偷地发送请求
$.ajax({ url:'', # 控制后端提交路径 type:'', # 控制请求方式 data:{}, # 控制提交的数据 dataType:"JSON", # django后端用HttpResponse返回json格式字符串,args不会自动反序列化,拿到的还是json格式字符串string字符类型,而如果是用JsonResponse返回的那么args会自动返序列化成前端js的对象类型 success:function(args){ # 异步回调机制 }) def index(request): if request.method == 'POST': back_dic = {'msg':'hahaha'} return HttpResponse(json.dumps(back_dic)) # 需要 return JsonResponse(back_dic) # 不需要 return render(request,'index.html') # 后续在写ajax请求的时候建议你加上dataType参数
2. 队列:先进先出,Python中有queue帮我们实现
import queue # 创建一个队列 q = queue.Queue() # 往队列中添加数据 q.put(111) q.put(222) # 从队列中取数据 v1 = q.get() v2 = q.get() # v3 = q.get() # 没有数据原地阻塞直到有数据 # v4 = q.get_nowait() # 没有数据直接报错 try: v5 = q.get(timeout=3) # 没有数据等待10s再没有就报错 queue.Empty except queue.Empty as e: pass print(v1,v2) # 实际生产中不会使用上述的消息队列 会使用功能更加的强大的 """ 消息队列 redis kafka rebittMQ """
服务端给每一个客户端维护一个队列,然后再浏览器上面通过ajax请求朝对应队列获取数据,没有数据就原地阻塞(pending状态),有就直接拿走渲染即可
3. 递归:Python的递归深度官网是1000,是没有递归优化的,js中没有递归概念,可以自己调自己
# python中有最大递归限制 997 998 官网给出的是1000 """ 在python中是没有尾递归优化的!!! """ def func(): func() func() # 不行 # 在js中 是没有递归的概念的 函数可以自己调用自己 属于正常的事件机制 function func1(){ $.ajax({ url:'', type:'', data:'', dataType:'JSON', success:function({ func1() # 可以 }) }) } func1()
import queue q_dict = {} # {唯一标示:对应的队列,唯一标示:对应的队列} def home(request): # 获取客户端浏览器的唯一标识 name = request.GET.get('name') # 生成一一对应关系 q_dict[name] = queue.Queue() return render(request,'home.html',locals()) def send_msg(request): if request.method == 'POST': # 获取用户发送的消息 message = request.POST.get('content') print(message) # 将消息给所有的队列发送一份 for q in q_dict.values(): q.put(message) return HttpResponse('OK') 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) back_dic['msg'] = data except queue.Empty as e: back_dic['status'] = False return JsonResponse(back_dic)
<h1>聊天室:{{ name }}</h1> <input type="text" id="txt"> <button onclick="sendMsg()">提交</button> <h1>聊天记录</h1> <div class="record"> </div> <script> function sendMsg() { // 朝后端发送消息 $.ajax({ url:'/send_msg/', type:'post', dataType:'JSON', data:{'content':$('#txt').val()}, success:function (args) { } }) } function getMsg() { // 偷偷的朝服务端要数据 $.ajax({ url:'/get_msg/', type:'get', data:{'name':'{{ name }}'}, success:function (args) { if (args.status){ // 获取消息 动态渲染到页面上 // 1 创建一个p标签 var pEle = $('<p>'); // 2 给p标签设置文本内容 pEle.text(args.msg); // 3 将p标签添加到div内部 $('.record').append(pEle) } getMsg() } }) } // 页面加载完毕立刻执行 $(function () { getMsg() }) </script>
2. 真实现
- Websocket(主流浏览器都支持)
网络协议:
- HTTP:不加密传输
- https:加密传输
- websocket:加密传输,浏览器与服务端创建连接后默认不断开(TCP 客户端与服务端都可以recv和send),解决了服务端真正的主动给客户端发送消息
3. websocket原理
websoket 实现原理可以分为两部分
1. 握手环节(handshake): 用握手环节来验证服务端是否支持websocket
- 浏览器访问服务端后浏览器会生成一个随机字符串
- 浏览器将生成的随机字符串发送给服务端(基于HTTP协议放在请求头中),自己也保留一份
- 服务端和客户端都对该字符串进行处理:跟magic string(固定的)拼接后加密(sha1+base64)
- 服务端将生成的加密字符串发给浏览器(基于HTTP,响应头中),做比对,如果一致说明服务端支持websocket,如果不一致说明不支持websocket
2. 收发数据环节
基于网络传输的数据都是二进制的
- 先读取第二个字节后七位数据(payload)根据payload做不同的处理
- 后七位数据如果是:
- =127:继续往后读8个字节(数据报总共10个字节)
- =126:继续往后读两个字节(数据报总共4个字节)
- <=125:不再往后读取(数据报总共2个字节)
- 上述操作完成后会再往后读固定的4个字节数据(masking-key)
- 依据masking-key解析出真实的数据
import socket import hashlib import base64 # 正常的socket代码 sock = socket.socket() # 默认就是TCP # 避免mac本重启服务经常报地址被占用的错误 sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) sock.bind(('127.0.0.1', 8080)) sock.listen(5) conn, address = sock.accept() data = conn.recv(1024) # 获取客户端发送的消息 # print(data.decode('utf-8')) def get_headers(data): """ 将请求头格式化成字典 :param data: :return: """ header_dict = {} data = str(data, encoding='utf-8') header, body = data.split('\r\n\r\n', 1) header_list = header.split('\r\n') for i in range(0, len(header_list)): if i == 0: if len(header_list[i].split(' ')) == 3: header_dict['method'], header_dict['url'], header_dict['protocol'] = header_list[i].split(' ') else: k, v = header_list[i].split(':', 1) header_dict[k] = v.strip() return header_dict def get_data(info): """ 按照websocket解密规则针对不同的数字进行不同的解密处理 :param info: :return: """ payload_len = info[1] & 127 if payload_len == 126: extend_payload_len = info[2:4] mask = info[4:8] decoded = info[8:] elif payload_len == 127: extend_payload_len = info[2:10] mask = info[10:14] decoded = info[14:] else: extend_payload_len = None mask = info[2:6] decoded = info[6:] bytes_list = bytearray() for i in range(len(decoded)): chunk = decoded[i] ^ mask[i % 4] bytes_list.append(chunk) body = str(bytes_list, encoding='utf-8') return body header_dict = get_headers(data) # 将一大堆请求头转换成字典数据 类似于wsgiref模块 client_random_string = header_dict['Sec-WebSocket-Key'] # 获取浏览器发送过来的随机字符串 magic_string = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' # 全球共用的随机字符串 一个都不能写错 value = client_random_string + magic_string # 拼接 ac = base64.b64encode(hashlib.sha1(value.encode('utf-8')).digest()) # 加密处理 tpl = "HTTP/1.1 101 Switching Protocols\r\n" \ "Upgrade:websocket\r\n" \ "Connection: Upgrade\r\n" \ "Sec-WebSocket-Accept: %s\r\n" \ "WebSocket-Location: ws://127.0.0.1:8080\r\n\r\n" response_str = tpl %ac.decode('utf-8') # 处理到响应头中 # 基于websocket收发消息 conn.send(bytes(response_str,encoding='utf-8')) while True: data = conn.recv(1024) # print(data) # 加密数据 b'\x81\x89\n\x94\xac#\xee)\x0c\xc6\xaf)I\xb6\x80' value = get_data(data) print(value)
前端代码:
<script> var ws = new WebSocket('ws://127.0.0.1:8080/') // 这一句话帮你完成了握手环节所有的操作 // 1 生成随机字符串 // 2 对字符串做拼接和加码操作 // 3 接受服务端返回的字符串做比对 </script>
4. Django中使用channels模块支持websocket
后端框架有的是不支持websocket的,是需要借助模块来实现的
""" 后端框架 django 默认不支持websocket 第三方模块:channels flask 默认不支持websocket 第三方模块:geventwebsocket tornado 默认支持websocket """
Django下载第三方模块:channels
需要注意的点:
1. 版本不要用最新版的,推荐使用2.3,如果你安装最新版可能会出现自动将你本地的django版本升级为最新版
2. Python解释器最好使用3.6版本,不会带来未知错误
channels模块内部帮你封装了握手:加密解密等操作
安装:pip3 install channels==2.3
基本使用
1. 注册app
INSTALLED_APPS = [ 'channels' ]
# 2 配置变量 ASGI_APPLICATION = 'sendsys.routing.application' # ASGI_APPLICATION = '项目名同名的文件名.文件夹下py文件名默认就叫routing.该py文件内部的变量名默认就叫application'
3. 去项目名同名的文件夹下建routing文件,定义application变量
from channels.routing import ProtocolTypeRouter,URLRouter from django.conf.urls import url from app01 import consumers application = ProtocolTypeRouter({ 'websocket':URLRouter([ # 书写websocket路由与视图函数对应关系 url(r'^index/',consumers.XXXClass) # CBV ]) })
上述配置完成后Django就即支持HTTP也支持websocket了,并且Django由wsgiref变为asgi启动
http 的在路由在urls.py中写,视图函数在views.py中写
websocket 路由在routing.py中写,视图函数在views.py的文件夹下新建一般叫做consumers.py文件中写, 只支持CBV