前戏
一般的IT互联网公司都会有一套自己的代码发布系统
目前来说大部分代码是基于运维jenkins来实现(shell脚本),其实也有公司自己定制自己的代码发布系统,定制的时候可以基于很多其他的技术点(saltstack、java、PHP、python系统)
我们的代码发布项目虽然是给运维或者测试用的,但是设计到的技术点基本全部都是python相关,我们注重的是开发逻辑
内容概要
服务端主动给客户端推送消息
截至目前为止我们所写的web项目,基本上都是服务端与客户端基于HTTP协议实现数据交互
HTTP四大特性:无链接(我请求你 你给我响应 之后我俩就没有关系了)并且都是浏览器主动请求服务端,服务端被动的作出响应
实现思路:
让浏览器每隔一段时间偷偷的朝服务器发送请求(ajax) 请求数据:轮询/长轮询 伪实现
目前比较火的websocket,创建链接之后不断开,真正实现
应用场景:
- 大屏幕投票
- 任务执行流程
gojs插件
前端插件,可以动态的生成图表、流程图、组织架构图等等
paramiko模块
类似于Xshell远程连接服务器并操作,底层用的是SSH连接
还会封装一个类,让操作更加方便(直接拷贝代码即可)
gitpython模块
通过python代码操作git
还会封装一个类,让操作更加方便(直接拷贝代码即可)
前期知识回顾
1.ajax操作
异步提交、局部刷新
$.ajax({
url:'', // 控制提交地址
type:'', // 控制提交方式
data:{}, // 控制提交数据
dataType:'JSON',
success:function(args){
res = JSON.parse(args)
// 异步回调函数
}
})
dataType参数
"""
django后端如果返回的是HttpResonse对象 前端的回调函数不会将json格式字符串转换成前端的自定义对象;
如果是JsonResponse对象 那么则会自动转换
配置该参数之后 无论什么情况都会自动将后端json字符串转换成前端自定义对象数据
所以后面写ajax的时候,建议将该参数配置上
"""
ps:django的每个应用都可以有自己的urls.py,views.py(还可以根据功能的不同划分成不同的py文件),templates模版文件夹(模版的查找的顺序,是按照配置文件中注册的app的顺序从上往下一次查找)
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01.apps.App01Config',
]
2.队列
ps:数据结构与算法必考
链表:判断链表是否有环、约瑟夫问题
算法:快排、堆排序原理、树
队列:先进先出
堆栈:先进后出
python内部帮我们维护了一个队列,但是实际生产,不会用到该模块,该模块只用于本地测试;而是使用redis、kafak、rabbitMQ(消息队列)
from django.test import TestCase
# Create your tests here.
import queue
# 创建一个队列
q = queue.Queue()
# 往队列中添加数据
q.put(111)
q.put(222)
# 去队列中取数据
v1 = q.get()
v2 = q.get()
try:
v4 = q.get(timeout=3) # 没有数据 等待10s之后才报错 queue.Empty
print(v4)
except queue.Empty as e:
pass
# v3 = q.get() # 一旦获取不到数据 默认是阻塞
# q.get_nowait() # 一旦没有数据立马报错
print(v1,v2)
思考
基于ajax和队列,我们能否实现服务端给客户端推送消息的感觉(伪)
服务端给每一个客户端浏览器维护一个队列,然后浏览器上面通过ajax不停的朝服务端请求数据(请求每个客户端浏览器对应的队列中的数据)没有数据则原地阻塞(前端浏览器查看网络状态pending)
只要有一个客户端给服务端发送了消息,服务端就将该消息给每一个队列一份
3.递归
# python中最大递归深度 997 998 官网提供的答案是1000
"""python中是没有尾递归优化的"""
def func():
func()
func()
# 在前端js中 是没有递归一说的 函数自己调用自己是可以的 属于一种事件处理完全OK
function func(){
$.ajax({
url:'', // 控制提交地址
type:'', // 控制提交方式
data:{}, // 控制提交数据
dataType:'JSON',
success:function(args){
func()
}
})
}
func()
4.modelform校验性组件
是forms组件的加强版本,功能也是三大块
校验数据、渲染标签、展示错误信息,快速的完成数据的增删改查
今日内容详细
服务端向客户端推送消息
伪实现
-
轮询(效率低 基本不用)
""" 轮询即轮番询问 让浏览器定时(例如每隔5s中发送一次)通过ajax偷偷滴朝服务端发送请求获取数据 不足之处 消息延迟 请求次数过多 损耗资源严重 """
-
长轮询(兼容性好)
""" 服务端给每个客户端创建一个队列,让浏览器通过ajax偷偷的发送请求,请求各自队列中的数据,如果没有数据则会阻塞但是不会一直阻塞 利用timeout参数加异常处理的形式最多阻塞30s之后返回,浏览器判断是否有数据,没有则继续发送(目前网页版的微信和qq用的还是这个原理) 相对于轮询 没有消息延迟的 请求次数降低了 资源损耗偏小 """
长轮询实现简易版本的群聊功能
# 长轮询实现聊天室功能 url(r'^home/$',views.home), url(r'^send_msg/$',views.send_msg), url(r'^get_msg/$',views.get_msg)
# 定义一个存储用户队列的字典 q_dict = {} # {'jason':队列} 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': # 获取用户输入的内容 msg = request.POST.get('msg') # 将该消息给所有的队列发送一份 for q in q_dict.values(): # 循环获取所有客户端浏览器对应的队列对象 q.put(msg) return HttpResponse('OK') def get_msg(request): # 获取用户唯一标示 去用户自己对应的队列中获取数据并非返回 name = request.GET.get('name') # 去全局字典中获取对应的队列 q = q_dict.get(name) # ajax交互一般用的都是字典格式 back_dic = {'status':True,'msg':''} # 异常处理 获取队列中的数据 try: data = q.get(timeout=10) # 等10s back_dic['msg'] = data except queue.Empty as e: # 将状态改为False 然后让浏览器再来要数据 back_dic['status'] = False return JsonResponse(back_dic)
<h1>聊天室:{{ name }}</h1> <div> <input type="text" id="i1"> <button onclick="sendMsg()">发送</button> </div> <h1>聊天纪录</h1> <div class="record"> </div> <script> function sendMsg() { $.ajax({ url:'/send_msg/', type:'post', data:{'msg':$('#i1').val(),'csrfmiddlewaretoken':'{{ csrf_token }}'}, success:function (args) { } }) } // 书写ajax偷偷的请求数据 自己跟自己的队列去要 function getMsg() { $.ajax({ url:'/get_msg/', type:'get', dataType:'JSON', // 携带唯一标示 data:{'name':'{{ name }}','csrfmiddlewaretoken':'{{ csrf_token }}'}, success:function (args) { // 判断是否有数据回来了 如果没有 则继续调用自身 if(args.status){ // 有消息则做消息展示 {#alert(status.msg)#} // 将消息通过DOM操作展示到前端页面 // 1 创建标签 var pEle = $('<p>'); // 2 添加文本内容 pEle.text(args.msg); // 3 将标签添加到页面对应的位置 $('.record').append(pEle) } // 如果args.status==False,继续调用自己 getMsg() } }) } // 页面加载完毕之后 就应该循环触发 onload 所有看似不起眼的知识点都是很有用的 $(function () { getMsg() }) </script>
大公司可能一般情况下都是采用长轮询,因为兼容性好
比如网页版的微信,qq等社交页面
真实现
-
websocket(主流浏览器都支持)
简介
""" 网络协议 HTTP 不加密传输 HTTPS 加密传输 上面两个协议都是短连接 websocket 加密传输 浏览器于服务端建立连接之后默认不断开,两端都可以基于该链接收发消息 websocket协议诞生 真正意义上实现了服务端给客户端推送消息 """
内部原理
""" websocket内部原理大致可以分为两部分 1.握手环节:验证服务端是否支持websocket协议 浏览器访问服务端 浏览器会自动生成一个随机字符串,然后将该字符串自己保留一份给服务端也发送一份,这一阶段的数据交互是基于HTTP协议的(该随机字符串是放在请求头中的) GET / HTTP/1.1 Host: 127.0.0.1:8080 Connection: Upgrade ... Sec-WebSocket-Key: kQHq6MzLH7Xm1rSsAyiD8g== Sec-WebSocket-Extensions: permessage-deflate; client_max_window_bits 浏览器和服务端手上都有随机字符串 服务端从请求头中获取随机字符串之后,会先拿该字符串跟magic string(固定的随机字符串)做字符串的拼接,会对拼接之后的数据进行加密处理(sha1/base64) 于此同时浏览器那边也会做相同的操作 服务端将处理好的随机字符串再次发送给浏览器(响应头) 浏览器会比对自己生成的随机字符串和服务端发送的随机字符串是否一致,如果一致,说明支持websocket协议,如果不一致则会报错不支持 2.收发数据:密文传输 数据解密 ps: 1.基于网络传输 数据都是二进制格式(python中bytes类型) 2.单位换算 数据解密 1.先读取第二个字节的后七位二进制数(payload) 2.根据payload不同做不同的处理 =127:继续向后读8个字节 =126:继续向后读2个字节 <=125:不再往后读取 3.之后再往后读取固定长度的4个字节的数据(masking-key) 根据该值计算出真实数据 """ # 这些原理了解即可 关键需要说出几个关键字 握手环节 magic string sha1/base64 127、126、125 payload masking-key
代码验证(无需掌握)
import socket import hashlib import base64 # 正常的socket代码 sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 防止mac/linux在重启的时候 报端口被占用的错 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) # 获取客户端发送的消息 def get_headers(data): """ 将请求头格式化成字典 :param data: :return: """ header_dict = {} data = str(data, encoding='utf-8') header, body = data.split(' ', 1) header_list = header.split(' ') 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 " "Upgrade:websocket " "Connection: Upgrade " "Sec-WebSocket-Accept: %s " "WebSocket-Location: ws://127.0.0.1:8080 " response_str = tpl %ac.decode('utf-8') # 处理到响应头中 # 将随机字符串给浏览器返回回去 conn.send(bytes(response_str, encoding='utf-8')) # 收发数据 while True: data = conn.recv(1024) # print(data) value = get_data(data) print(value)
<script> var ws = new WebSocket('ws://127.0.0.1:8080/'); // 这一行代码干了很多事 // 1.自动生成随机字符串 // 2.自动处理随机字符串 magic string sha1/base64 // 3.自动比对 </script>
这是内部原理,我们实际生产不需要写这些代码,直接使用封装好的模块即可
django实现websocket
"""
强调:
并不是所有的后端框架默认都支持websocket
在django中如果你想要开发websocket相关的功能,需要安装模块
pip3 install channels==2.3
注意事项
1.不要直接安装最新版本的channels,可能会自动将你的django版本升级为最新版
2.python解释器环境建议使用3.6(官网的说法:3.5可能会出现问题,3.7也可能会出现问题...具体说明问题没有说!)
channels内部已经帮你封装好了上面代码演示所有的过程
握手 加密 解密
python三大主流web框架对websocket的支持
django
默认不支持
第三方模块:channels
flask
默认不支持
第三方模块:geventwebsocket
tornado
默认就支持
"""
如何实现
1.注册channels
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'app01.apps.App01Config',
'channels'
]
启动django项目会报错CommandError: You have not set ASGI_APPLICATION, which is needed to run the server.
2.配置参数
ASGI_APPLICATION = 's12_day01.routing.application'
# ASGI_APPLICATION = '项目名同名的文件名称.routing.py文件名.application变量名'
3.项目名同名的文件夹下创建routing.py文件并书写固定代码
from channels.routing import ProtocolTypeRouter,URLRouter
application = ProtocolTypeRouter({
'websocket':URLRouter([
# websocket相关的url与视图函数对应关系
])
})
上述三步配置完成后,再次启动django,就会即支持http协议又支持websocket协议
之后关于http的url与视图函数对应关系还是在原来的urls.py中书写
关于websocket的url与视图函数对应关系则在routing.py中书写