代碼發布
服務端主動給客戶端推送消息
截至目前為止,我們所寫的 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

