WebSocket 實現服務端給客戶端推送消息


代碼發布

服務端主動給客戶端推送消息

截至目前為止,我們所寫的 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
第三方模塊: channels

flask
默認不支持 websocket
第三方模塊: geventwebsocket

tornado
默認支持 websocket


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM