前戲
一般的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內部幫我們維護了一個隊列
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) } // 繼續調用自己 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('\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') # 處理到響應頭中 # 將隨機字符串給瀏覽器返回回去 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中書寫