服務端和客戶端通信的三種方式
輪詢
客戶端每隔一段時間向服務器發送ajax請求,看是否有新的消息
缺點
延遲為時間間隔,請求次數多
長輪詢
服務端給每一個客戶端建立隊列,如果客戶端發起ajax請求,就會去各自對應的隊列中去獲取數據,如果沒有數據就阻塞,但是不會一直阻塞,可以設置阻塞的時間timeout,比如阻塞30s后返回響應,觸發客戶端的回調函數,客戶端再次發起ajax請求
相對於輪詢
消息基本沒有延遲
請求減少
兼容性好(web的qq和微信都是基於長輪詢的)
websocket
注意
websocket是一種加密的網絡協議
協議原理
主要分為兩階段:握手階段和收發數據階段
#握手環節(handshake)
目的:驗證服務端是否支持websocket協議
客戶端瀏覽器第一次訪問服務端的時候
瀏覽器內部會自動生成一個隨機字符串,將該隨機字符串發送給服務端(基於http協議),自己也保留一份(請求頭里面)
服務端接受到隨機字符串之后,會讓它跟magic string(全球統一)做字符串的拼接
然后利用加密算法對拼接好的字符串做加密處理(sha1/base64)
客戶端也在對產生的隨機字符串做上述的拼接和加密操作
服務單將產生好的隨機字符串發送給客戶端瀏覽器(響應頭里面)
客戶端瀏覽器會比對服務單發送的隨機字符串很我瀏覽器本地操作完的隨機字符串是否一致,如果一致說明該服務端支持websocket,如果不一致則不支持
#收發數據(send/onmessage)
驗證成功之后就可以數據交互了 但是交互的數據是加密的 需要解密處理
前提:
1.數據基於網絡傳輸都是二進制格式
2.單位換算 8bit = 1bytes
讀取第二個字節的后七位稱之為payload
1.根據payload大小決定不同的處理方式
=127 再讀取8個字節 作為數據報
=126 再讀取2個字節 作為數據報
<=125 不再往后讀了
2.步驟1之后 會對剩下的數據再讀取4個字節(masking-key)
之后依據masking-key算出真實數據
var DECODED = "";
for (var i = 0; i < ENCODED.length; i++) {
DECODED[i] = ENCODED[i] ^ MASK[i % 4];
}
基於長輪詢的群聊Demo
界面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link href="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
<script src="https://cdn.bootcss.com/twitter-bootstrap/3.4.1/js/bootstrap.min.js"></script>
{% load staticfiles %}
<script src="{% static 'myjq.js' %}"></script>
</head>
<body>
<h1>聊天室:{{ name }}</h1>
<div>
<input type="text" name="content" id="d1">
<button id="d2">發送</button>
</div>
<h1>聊天紀錄</h1>
<div class="record">
</div>
<script>
// 朝后端發送用戶消息
$('#d2').click(function () {
$.ajax({
url:'/send_msg/',
type:'post',
data:{'content':$("#d1").val(),'csrfmiddlewaretoken':'{{ csrf_token }}'},
{#dataType:"JSON",#}
success:function (args) {
}
})
});
// 向后端請求數據代碼
function getMsg() {
$.ajax({
url:'/get_msg/',
type:'get',
data:{'name':'{{ name }}'},
{#dataType:"JSON",#}
success:function (args) {
if(args.status){
// 有消息 應該DOM操作渲染到頁面上
// 1 創建標簽
var pEle = $('<p>');
// 2 給標簽添加文本內容
pEle.text(args.msg);
// 3 添加到div中
$('.record').append(pEle)
}else{
// 沒有則繼續發送
}
getMsg()
}
})
}
// 頁面加載完畢之后自動觸發getMsg函數的執行,監聽是否有新的消息
$(function () { // 等待頁面加載完畢之后自動調用getMsg函數
getMsg()
})
</script>
</body>
</html>
views
from django.shortcuts import render,HttpResponse
import queue
# Create your views here.
#定義全局字典存儲瀏覽器和隊列的關系
q_dict = {}
def home(request):
#獲取用戶唯一標識,用於創建隊列
name = request.GET.get('name')
#給每個剛請求界面的用戶創建一個隊列
q_dict[name] = queue.Queue()
return render(request,'index.html',locals())
def send_msg(request):
if request.method == 'POST':
content = request.POST.get('content')
#把消息裝到所有的隊列中去
for q in q_dict.values():
q.put(content)
return HttpResponse('ok')
#長輪詢核心代碼
import json
from django.http import JsonResponse
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)
print(data)
back_dic['msg'] = data
except queue.Empty as e:
back_dic['status'] = False
return JsonResponse(back_dic)
websocket支持
django
-默認不支持
-下載第三方模塊:channels
flask
-默認不支持
-下載第三方模塊:geventwebsocket
tornado
-默認就是支持的
# 如果你想在django支持websocket的話 需要安裝對應的模塊
"""
在django安裝channles時候不要直接安裝最新版本 可能會自動將的解釋器中的django版本改為最新版
python解釋器環境推薦使用3.6(官網:3.5可能會出問題,3.7也可能會出問題,沒有給出具體的解釋)
"""
pip3 install channles==2.3
django使用websocket
1.setting配置
INSTALLED_APPS = [
# 1 注冊channles應用
'channels'
]
2.添加配置
ASGI_APPLICATION = 'qq.routing.application'
# ASGI_APPLICATION = '項目名同名的文件夾名.項目內部的py文件名(默認就叫routing,注意不在app里面).routing文件內的變量名(默認就叫application)'
3.application配置
#在routing.py里面寫下如下代碼
from channels.routing import ProtocolTypeRouter,URLRouter
from django.conf.urls import url
from app01 import consumers
application =ProtocolTypeRouter({
'websocket':URLRouter([
# websocket請求路由與視圖函數的對應關系
url(r'^chat/$',consumers.ChatConsumer)
])
})
總結:配置完成后django由原來默認的wsgiref替換成asgi啟動(asgi內部是基於達芙妮)
上述配置完成后 ,django就會同時支持http協議和websocket協議
原先基於http協議的路由與視圖函數對應關系還是跟之前一樣urls.py、views.py
針對websocket協議的路由與視圖函數對應關系則需要在routing.py和consumers.py(在對應的應用下創建即可)
業務邏輯代碼consumers
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
"""
客戶端請求鏈接之后自動觸發
:param message: 消息數據
"""
self.accept() # 建立鏈接
def websocket_receive(self, message):
"""
客戶端瀏覽器發送消息來的時候自動觸發
:param message: 消息數據 {'type': 'websocket.receive', 'text': 'hello'}
"""
text = message.get('text')
print(text)
def websocket_disconnect(self, message):
"""
客戶端斷開鏈接之后自動觸發
:param message:
"""
raise StopConsumer() # 主動報異常自動斷開鏈接 無需做處理 內部自動捕獲
js
<h1>聊天室</h1>
<div>
<input type="text" id="text" placeholder="請輸入">
<input type="button" value="發送" onclick="sendMsg()">
<input type="button" value="斷開鏈接" onclick="close()">
</div>
<h1>聊天紀錄</h1>
<div id="content">
</div>
<script>
var ws = new WebSocket('ws://127.0.0.1:8000/chat/');
// 1 發送消息 ws.send()
// 2 握手成功之后 自動觸發 ws.onopen
// 3 服務端發送消息過來 自動觸發 ws.onmessage
// 4 斷開鏈接 ws.close()
// 0 握手成功之后自動觸發
ws.onopen = function () {
alert('建立成功')
};
// 1 給服務端發送消息
function sendMsg() {
ws.send($('#text').val());
}
// 2 接受服務端發送過來的消息
ws.onmessage = function (event) { // event是對象
var dataValue = event.data; // 獲取服務端的數據
// 將數據動態渲染到頁面上
var pEle = $('<p>');
pEle.text(dataValue);
$('#content').append(pEle)
};
// 3 斷開鏈接
function close() {
ws.close()
}
</script>