如何實現服務端主動給客戶端推送消息的效果:
偽實現:
可不可以讓客戶端瀏覽器每隔一段事件偷偷的取服務器請求數據,但是內部本質還是客戶端朝服務器發送消息
輪詢
長輪詢
真實現:
Websocket
真正的實現了服務端主動給客戶端推送消息
一般應用的場景:
大屏幕股票的實時展示,群聊功能等等
ajax操作
異步提交,局部刷新
$.ajax({
url:'', # 控制后端提交路徑
type:'',# 控制請求的方式
data:{},# 控制提交的數據
dataType:"JSON",
#不加上這個, django后端用HttpResponse返回json格式字符串,args不會自動反序列化,拿到的還是json格式字符串string字符類型,而如果是用JsonResponse返回的那么args會自動返回序列化成前端js的對象類型
success:function(args){
# 放異步回調機制
}
})
def index(request):
if request.menthod == '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()
# q.get_nowait() 沒有數據會直接報錯
# q.get() 如果沒有數據會原地阻塞知道有數據
try:
v3 = q.get(timeout=3) # 沒有數據等待10s再沒有就報錯
except queue.Empty as e:
pass
一般在實際應用中不使用上述的隊列,一般是redis,ksfks等的消息隊列
基於ajax與隊列其實就可以實現服務端給客戶端推送消息的效果,服務端給每一個客戶端維護一個隊列,然后在瀏覽器上面通過ajax請求朝對應隊列獲取數據,沒有數據就原地阻塞(pending狀態),有就會直接拿走渲染
關於遞歸的問題
python中是有最大的遞歸限制,官網給的是1000,實際會根據電腦的配置來決定
在python中是沒有尾遞歸優化的
在js中是沒有遞歸的概念。函數自己調用自己,屬於正常的事件機制
輪詢
效率極低,基本不使用
讓瀏覽器定時(如每隔幾秒發一次)通過ajax朝服務端發送請求獲取數據
缺點:
消息延遲嚴重
請求次數多,消耗資源過大
長輪詢
服務端給每個瀏覽器創建一個隊列,讓瀏覽器通過ajax向后端偷偷的發送請求,去各自對應的隊列中獲取數據,如果沒有數據則會阻塞,但是不會一直阻塞,阻塞后會給一個響應,無論響應是否是真正的數據,都會再次通過回調函數調用請求數據的代碼
優點:
消息基本沒有延遲
請求次數降低,消耗資源變小
案例:基於ajxa,隊列以及異常處理時限簡易版本的群聊功能(長輪詢來實現)
后端:
view.py
import queue
q_dict = {} # {唯一標識: 對應的隊列}
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':
# 獲取用戶發送的信息
message = request.POST.get('content')
print(message)
# 將消息給所有的隊列發送一份
for q in q_dic.values():
q,put(message)
return HTTPResponse('ok')
def get_msg(request):
# 虎丘用戶唯一標識
name = request.GET.get('name')
# 回去對應的隊列
q = q_dic.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'] = Fasle
return JsonResponse(back_dic)
url.py
from django.conf.urls import url
from django.contrib import admin
from app01 import views
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^index/', views.index),
# 長輪詢
url(r'^home/', views.home),
url(r'^send_msg/', views.send_msg),
url(r'^get_msg/', views.get_msg)
]
前端
<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>
效果圖:
websocket
主流瀏覽器都支持,能夠真正的實現服務端給客戶端推送消息
網路協議
http 不加密傳輸
https 加密傳輸
上面兩個都是短鏈接/無鏈接
webSocket 加密傳輸
瀏覽器和服務端創建鏈接之后默認不斷開,能夠真正的實現服務端給客戶端推送消息
websocket內部的原理
關鍵字:magic string sha1/base64 payload(127,126,125),masking-key
websocket實現原理可以分為兩個部分
1.握手環節(handshake):並不是所有的服務器都支持websocket 所以用握手環節來驗證服務端是否支持websocket
2.收發數據環節:數據解密
1.握手環節
瀏覽器訪問服務端之后瀏覽器會立刻生成一個隨機字符串
瀏覽器會將生成好的隨機字符串發送給服務端(基於http協議,放在請求頭中),並且自己也保留一份
服務端和客戶端會對該隨機字符串進行一下處理:
1.1 先拿隨機字符串跟magic string(固定的字符串)做字符串的拼接
1.2 將拼接之后的結果做加密處理(sha1+base64)
服務端將生成好的處理結果發送給瀏覽器(基於http協議 放在響應頭中)
瀏覽器接收服務端發送過來的隨機字符串跟本地處理好的隨機字符串做比對,如果一致說明服務端支持websocket,如果不一致則說明不支持
2.收發數據環節
{
基於網絡傳輸數據都是二進制格式,在python中可以用bytes類型對應,實現進制換算
}
先讀取第二個字節的后七位數據(payload)根據payload做不同的處理:
=127:繼續往后讀取8個字節數據(數據報10個字節)
=126:繼續往后讀取2個字節數據(數據報4個字節)
<=125:不再往后讀取(數據2個字節)
上述操作完成后,會繼續往后讀取固定長度4個字節的數據(masking-key)
依據masking-key解析出真實數據
代碼驗證(詮釋websocket內部本質)
# 請求頭中的隨機字符串
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協議,如果想要去使用,需要借助於不同的第三方模塊
后端框架
django
默認不支持websocket
第三方模塊:channels
flask
默認不支持websocket
第三方模塊:geventwebsocket
tornado:
默認支持websocket
django如何取支持websocket
# 下載channels模塊需要注意的點
# 1.版本不要用最新版 推薦使用2.3版本即可 如果你安裝最新版可能會出現自動將你本地的django版本升級為最新版
# 2.python解釋器建議使用3.6版本(3.5可能會有問題,3.7可能會有問題 具體說明問題沒有給解釋)
pip3 install channels==2.3
channels模塊內部封裝了握手、加密,解密等的所有操作
基本的使用方法
1.注冊app
INSTALLED_APPS = [
'channels'
]
注冊完成后,django會無法啟動,會直接報錯
CommandError: You have not set ASGI_APPLICATION, which is needed to run the server.
2.配置
配置變量
ASGI_APPLICATION = '項目名同名的文件名.文件夾下py文件名默認就叫routing.該py文件內部的變量名默認就叫application'
ASGI_APPLICATION ='websocket.routing.application'
去項目名同名的文件夾下面新建一個py文件,定義application變量
from channels.routing import ProtocolTypeRouter,URLRouter
application = ProtocolTypeRouter({
'websocket':URLRouter([
# 書寫websocket路由與視圖函數對應關系
])
})
上述操作配置完成后,啟動django會由原來的wsgiref啟動變成asgi啟動(內部:達芙妮)
並且啟動之后django既支持websocket也支持http協議
基於http的操作還是在urls.py和views.py中完成
基於websocket的操作則在routing.py和consumer.py(對應的應用中創建)中完成