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


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

HTTP協議中的四大特性中有無鏈接:一次請求一次響應后斷開鏈接,因此基於HTTP協議實現服務端改客戶端主動推送消息有點麻煩

1. 服務端給客戶端主動推送消息有以下應用情景:

  • 大屏幕投票實時展示

  • 任務的執行流程

  • 群聊功能

  • 等等 ...

2. 如何實現服務端給客戶端主動推送消息

大的方向有兩種方式:

1. 偽實現

讓客戶端瀏覽器每隔一段時間偷偷的去服務器請求數據,這樣能實現效果,但是內部本質還是客戶端朝服務端發送消息

  • 輪詢

    """
    讓瀏覽器定時(例如每隔5秒發一次)通過ajax朝服務端發送請求獲取數據
    
    缺點:
        消息延遲嚴重
        請求次數多 消耗資源過大
    """
  • 長輪詢

    """
    服務端給每個瀏覽器創建一個隊列,讓瀏覽器通過ajax向后端偷偷的發送請求,去各自對應的隊列中獲取數據,如果沒有數據則會有阻塞,
    但是不會一直阻塞,比如最多阻塞30秒(pending)后給一個響應,無論響應是否是真正的數據,都會再次通過回調函數調用請求數據的代碼 優點: 消息基本沒有延遲 請求次數降低 消耗資源減少
    """ # 大公司需要考慮兼容性問題 追求兼容 目前網頁版本的微信和qq用的就是長輪詢

基於ajax、隊列、異常處理實現長輪詢簡易版群聊功能

1. 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參數
ajax回顧

2. 隊列:先進先出,Python中有queue幫我們實現

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
"""
queue隊列回顧

基於ajax與隊列其實就可以實現服務端給客戶端推送消息的效果

服務端給每一個客戶端維護一個隊列,然后再瀏覽器上面通過ajax請求朝對應隊列獲取數據,沒有數據就原地阻塞(pending狀態),有就直接拿走渲染即可

群聊:獲取群聊中某個人發送的消息,將該消息給每一個隊列

3. 遞歸:Python的遞歸深度官網是1000,是沒有遞歸優化的,js中沒有遞歸概念,可以自己調自己

# python中有最大遞歸限制 997 998 官網給出的是1000
"""
在python中是沒有尾遞歸優化的!!!
"""
def func():
  func()
func()  # 不行

# 在js中 是沒有遞歸的概念的 函數可以自己調用自己 屬於正常的事件機制
function func1(){
  $.ajax({
    url:'',
    type:'',
    data:'',
    dataType:'JSON',
    success:function({
      func1()  # 可以
    })
  })
}
func1()
js可以自己調自己,Python不行
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_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>
前端代碼

2. 真實現

  • Websocket(主流瀏覽器都支持)

網絡協議:

  • HTTP:不加密傳輸
  • https:加密傳輸
  • websocket:加密傳輸,瀏覽器與服務端創建連接后默認不斷開(TCP 客戶端與服務端都可以recv和send),解決了服務端真正的主動給客戶端發送消息

3. websocket原理

websoket 實現原理可以分為兩部分

1. 握手環節(handshake): 用握手環節來驗證服務端是否支持websocket

  1. 瀏覽器訪問服務端后瀏覽器會生成一個隨機字符串
  2. 瀏覽器將生成的隨機字符串發送給服務端(基於HTTP協議放在請求頭中),自己也保留一份
  3. 服務端和客戶端都對該字符串進行處理:跟magic string(固定的)拼接后加密(sha1+base64)
  4. 服務端將生成的加密字符串發給瀏覽器(基於HTTP,響應頭中),做比對,如果一致說明服務端支持websocket,如果不一致說明不支持websocket

2. 收發數據環節

基於網絡傳輸的數據都是二進制的

  1. 先讀取第二個字節后七位數據(payload)根據payload做不同的處理
  2. 后七位數據如果是:
    1. =127:繼續往后讀8個字節(數據報總共10個字節)
    2. =126:繼續往后讀兩個字節(數據報總共4個字節)
    3. <=125:不再往后讀取(數據報總共2個字節)
  3. 上述操作完成后會再往后讀固定的4個字節數據(masking-key)
  4. 依據masking-key解析出真實的數據
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)
后端代碼實現websocket

前端代碼:

<script>
    var ws = new WebSocket('ws://127.0.0.1:8080/')
    // 這一句話幫你完成了握手環節所有的操作
    // 1 生成隨機字符串
    // 2 對字符串做拼接和加碼操作
    // 3 接受服務端返回的字符串做比對
</script>

4. Django中使用channels模塊支持websocket

后端框架有的是不支持websocket的,是需要借助模塊來實現的

"""
后端框架
django
    默認不支持websocket
    第三方模塊:channels

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

Django下載第三方模塊:channels

需要注意的點:

1. 版本不要用最新版的,推薦使用2.3,如果你安裝最新版可能會出現自動將你本地的django版本升級為最新版

2. Python解釋器最好使用3.6版本,不會帶來未知錯誤

channels模塊內部幫你封裝了握手:加密解密等操作

安裝:pip3 install channels==2.3

基本使用

1. 注冊app

INSTALLED_APPS = [
    'channels'
]

注冊完成后,django會無法啟動,會直接報錯

CommandError: You have not set ASGI_APPLICATION, which is needed to run the server.

2. 配置

# 2 配置變量
ASGI_APPLICATION = 'sendsys.routing.application'
# ASGI_APPLICATION = '項目名同名的文件名.文件夾下py文件名默認就叫routing.該py文件內部的變量名默認就叫application'

3. 去項目名同名的文件夾下建routing文件,定義application變量

from channels.routing import ProtocolTypeRouter,URLRouter
from django.conf.urls import url
from app01 import consumers

application = ProtocolTypeRouter({
    'websocket':URLRouter([
        # 書寫websocket路由與視圖函數對應關系
        url(r'^index/',consumers.XXXClass)  # CBV
    ])
})

上述配置完成后Django就即支持HTTP也支持websocket了,並且Django由wsgiref變為asgi啟動

http 的在路由在urls.py中寫,視圖函數在views.py中寫

websocket 路由在routing.py中寫,視圖函數在views.py的文件夾下新建一般叫做consumers.py文件中寫, 只支持CBV

 


免責聲明!

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



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