python web服务端主动推送消息到浏览器页面的具体实现代码
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了python web服务端主动推送消息到浏览器页面的具体实现代码相关的知识,希望对你有一定的参考价值。
就是是将 服务端的 处理后的数据实时的推送到 浏览器,与 web 聊天室一样的效果
参考技术A 0如果想从服务器端推送消息到客户端的话,个人觉得GoEasy推送还不错,它有Restful API 支持多语言,而且针对java他们还特别做了SDK,使用很简单方便。同时它也支持客户端推送。由于它支持websocket 和polling两种连接方式所以兼顾大多数主流浏览器,低版本的IE浏览器也是支持的,很适合需要快速建立实时web通信的项目。追问
主要我们使用的是 python 并不是java
WebSocket 实现服务端给客户端推送消息
代码发布
服务端主动给客户端推送消息
截至目前为止,我们所写的 web 项目基本都是基于 HTTP 协议的
HTTP 协议有四大特性:无链接
基于 HTTP 协议实现服务端主动给客户端推送消息好像有点麻烦~~~
我们都经历过,浏览器打开一个网站不动,网站过一会儿自动弹出消息
再比如网页版本的微信和 qq,我们所有人创建一个群聊,所有人加入群聊之后都不动
我朝群中发送一个消息,你们所有人的页面上都会出现我发送的消息
应用场景
- 大屏幕投票实时展示
- 任务的执行流程
- 群聊功能
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参数
队列
队列:先进先出
堆栈:先进后出
python 内部在内存中帮我们维护了一个队列
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 与队列其实就可以实现服务端给客户端推送消息的效果
服务端给每一个客户端维护一个队列,然后再浏览器上面通过 ajax 请求朝对应队列获取数据,没有数据就原地阻塞(pending状态),有就直接拿走渲染即可
群聊:获取群聊中某个人发送的消息,将该消息给每一个队列
递归
# python中有最大递归限制 997 998 官网给出的是1000 """ 在python中是没有尾递归优化的!!! """ def func(): func() func() # 不行 # 在js中 是没有递归的概念的 函数可以自己调用自己 属于正常的事件机制 function func1(){ $.ajax({ url:\'\', type:\'\', data:\'\', dataType:\'JSON\', success:function({ func1() # 可以 }) }) } func1()
校验性组件
forms 组件
modelform 组件(它是forms组件的加强版本,功能和代码差不多,但是更加的方便)
如何实现服务端主动给客户端推送消息的效果
伪实现
可不可以让客户端浏览器每隔一段时间偷偷的去服务器请求数据
这样能实现效果,但是内部本质还是客户端朝服务端发送消息
- 轮询
- 长轮询
真实现
- Websocket
它的诞生真正的实现了服务端主动给客户端推送消息
轮询(效率极低,基本不用)
让浏览器定时(例如每隔 5 秒发一次)通过 ajax 朝服务端发送请求获取数据
缺点:
消息延迟严重
请求次数多 消耗资源过大
长轮询(兼容性好)
服务端给每个浏览器创建一个队列,让浏览器通过 ajax 向后端偷偷的发送请求,去各自对应的队列中获取数据,如果没有数据则会有阻塞,但是不会一直阻塞,比如最多阻塞 30 秒(pending)后给一个响应,无论响应是否是真正的数据,都会再次通过回调函数调用请求数据的代码
优点:
消息基本没有延迟
请求次数降低 消耗资源减少大公司需要考虑兼容性问题 追求兼容 目前网页版本的微信和 qq 用的就是长轮询
ps:给标签绑定事件的方式大致有两种
1 标签查找绑定
$(\'p\').click()
2 直接写函数 注意括号不能少
<p onclick="sendMsg()"></p>
基于 ajax,队列以及异常处理实现简易版本的群聊功能(长轮询)
后端
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)
前端
<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>
websocker(主流浏览器都支持)
网络协议
HTTP 不加密传输
HTTPS 加密传输
上面两个都是短链接/无链接
WebSocket 加密传输
浏览器和服务端创建链接之后默认不断开(联想网络编程TCP recv和send方法)
它的诞生能够真正的实现 服务端给客户端推送消息
内部原理
websocket 实现原理可以分为两部分
1 握手环节(handshake):并不是所有的服务端都支持 websocket 所以用握手环节来验证服务端是否支持 websocket
2 收发数据环节:数据解密
握手环节
浏览器访问 服务端之后,浏览器会立刻生成一个随机字符串
浏览器会将生成好的随机字符串发送给服务端(基于 HTTP 协议 放在请求头中),并且自己也保留一份
服务端和客户端都会对该随机字符串做以下处理
- 先拿随机字符串跟 magic string (固定的字符串)做字符串的拼接
- 将拼接之后的结果做加密处理 (sha1+base64)
服务端将生成好的处理结果发送给浏览器(基于 HTTP 协议 放在响应头中)
浏览器接受服务端发送过来的随机字符串,跟本地处理好的随机字符串做比对,如果一致说明服务端支持 websocket,如果不一致说明不支持
收发数据环节
前提知识点:
1.基于网络传输数据都是二进制格式,在 python 中可以用 bytes 类型对应
2.进制换算先读取第二个字节的后七位数据 (payload) 根据 payload 做不同的处理
=127:继续往后读取 8 个字节数据(数据报10个字节)
=126:继续往后读取2个字节数据(数据报4个字节)
<=125:不再往后读取(数据2个字节)
上述操作完成后,会继续往后读取固定长度4个字节的数据 (masking-key)
依据 masking-key 解析出真实数据
关键字:sha1/base64、magic string、payload(127,126,125)、masking-key
代码验证(了解)
# 请求头中的随机字符串 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)
<script> var ws = new WebSocket(\'ws://127.0.0.1:8080/\') // 这一句话帮你完成了握手环节所有的操作 // 1 生成随机字符串 // 2 对字符串做拼接和加密操作 // 3 接受服务端返回的字符串做比对 </script>
总结:上述代码知识为了诠释 websocket 内部本质,实际应用直接使用别人封装好的模块即可
实际应用中,并不是所有的后端框架默认都支持 websocket 协议,如果你想使用的话,可能需要借助于不同的第三方模块
后端框架
django
默认不支持 websocket
第三方模块: channelsflask
默认不支持 websocket
第三方模块: geventwebsockettornado
默认支持 websocket
以上是关于python web服务端主动推送消息到浏览器页面的具体实现代码的主要内容,如果未能解决你的问题,请参考以下文章