WebSocket 实现服务端给客户端推送消息
2021-03-23 05:24
标签:页面 channel form split dump dex 封装 兼容性 问题 服务端主动给客户端推送消息 截至目前为止,我们所写的 web 项目基本都是基于 HTTP 协议的 HTTP 协议有四大特性:无链接 基于 HTTP 协议实现服务端主动给客户端推送消息好像有点麻烦~~~ 我们都经历过,浏览器打开一个网站不动,网站过一会儿自动弹出消息 再比如网页版本的微信和 qq,我们所有人创建一个群聊,所有人加入群聊之后都不动 我朝群中发送一个消息,你们所有人的页面上都会出现我发送的消息 异步提交,局部刷新 用它就可以偷偷的朝服务端发送请求 队列:先进先出 堆栈:先进后出 python 内部在内存中帮我们维护了一个队列 基于 ajax 与队列其实就可以实现服务端给客户端推送消息的效果 服务端给每一个客户端维护一个队列,然后再浏览器上面通过 ajax 请求朝对应队列获取数据,没有数据就原地阻塞(pending状态),有就直接拿走渲染即可 群聊:获取群聊中某个人发送的消息,将该消息给每一个队列 校验性组件 forms 组件 modelform 组件(它是forms组件的加强版本,功能和代码差不多,但是更加的方便) 伪实现 可不可以让客户端浏览器每隔一段时间偷偷的去服务器请求数据 这样能实现效果,但是内部本质还是客户端朝服务端发送消息 真实现 它的诞生真正的实现了服务端主动给客户端推送消息 轮询(效率极低,基本不用) 让浏览器定时(例如每隔 5 秒发一次)通过 ajax 朝服务端发送请求获取数据 缺点: 消息延迟严重 服务端给每个浏览器创建一个队列,让浏览器通过 ajax 向后端偷偷的发送请求,去各自对应的队列中获取数据,如果没有数据则会有阻塞,但是不会一直阻塞,比如最多阻塞 30 秒(pending)后给一个响应,无论响应是否是真正的数据,都会再次通过回调函数调用请求数据的代码 优点: 消息基本没有延迟 大公司需要考虑兼容性问题 追求兼容 目前网页版本的微信和 qq 用的就是长轮询 ps:给标签绑定事件的方式大致有两种 1 标签查找绑定 2 直接写函数 注意括号不能少 基于 ajax,队列以及异常处理实现简易版本的群聊功能(长轮询) 后端 前端 网络协议 HTTP 不加密传输 HTTPS 加密传输 上面两个都是短链接/无链接 WebSocket 加密传输 内部原理 websocket 实现原理可以分为两部分 1 握手环节(handshake):并不是所有的服务端都支持 websocket 所以用握手环节来验证服务端是否支持 websocket 握手环节 浏览器访问 服务端之后,浏览器会立刻生成一个随机字符串 浏览器会将生成好的随机字符串发送给服务端(基于 HTTP 协议 放在请求头中),并且自己也保留一份 服务端和客户端都会对该随机字符串做以下处理 服务端将生成好的处理结果发送给浏览器(基于 HTTP 协议 放在响应头中) 浏览器接受服务端发送过来的随机字符串,跟本地处理好的随机字符串做比对,如果一致说明服务端支持 websocket,如果不一致说明不支持 收发数据环节 前提知识点: 先读取第二个字节的后七位数据 (payload) 根据 payload 做不同的处理 =127:继续往后读取 8 个字节数据(数据报10个字节) =126:继续往后读取2个字节数据(数据报4个字节)
上述操作完成后,会继续往后读取固定长度4个字节的数据 (masking-key) 依据 masking-key 解析出真实数据 关键字:sha1/base64、magic string、payload(127,126,125)、masking-key 总结:上述代码知识为了诠释 websocket 内部本质,实际应用直接使用别人封装好的模块即可 实际应用中,并不是所有的后端框架默认都支持 websocket 协议,如果你想使用的话,可能需要借助于不同的第三方模块 后端框架 flask tornado WebSocket 实现服务端给客户端推送消息 标签:页面 channel form split dump dex 封装 兼容性 问题 原文地址:https://www.cnblogs.com/kai-/p/12692187.html
代码发布
应用场景
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参数
队列
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
"""
递归
# python中有最大递归限制 997 998 官网给出的是1000
"""
在python中是没有尾递归优化的!!!
"""
def func():
func()
func() # 不行
# 在js中 是没有递归的概念的 函数可以自己调用自己 属于正常的事件机制
function func1(){
$.ajax({
url:‘‘,
type:‘‘,
data:‘‘,
dataType:‘JSON‘,
success:function({
func1() # 可以
})
})
}
func1()
如何实现服务端主动给客户端推送消息的效果
请求次数多 消耗资源过大长轮询(兼容性好)
请求次数降低 消耗资源减少$(‘p‘).click()
import queue
q_dict = {} # {唯一标示:对应的队列,唯一标示:对应的队列}
def home(request):
# 获取客户端浏览器的唯一标识
name = request.GET.get(‘name‘)
# 生成一一对应关系
q_dict[name] = queue.Queue()
return render(request,‘home.html‘,locals()) # 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)
聊天室:{{ name }}
聊天记录
websocker(主流浏览器都支持)
浏览器和服务端创建链接之后默认不断开(联想网络编程TCP recv和send方法)
它的诞生能够真正的实现 服务端给客户端推送消息
2 收发数据环节:数据解密
1.基于网络传输数据都是二进制格式,在 python 中可以用 bytes 类型对应
2.进制换算代码验证(了解)
# 请求头中的随机字符串
Sec-WebSocket-Key: NlNG/FK/FrQS/RH5Bcy9Gw==
# 响应头
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‘) # 处理到响应头中
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)
django
默认不支持 websocket
第三方模块: channels
默认不支持 websocket
第三方模块: geventwebsocket
默认支持 websocket