Django與Channels實現WebSocket

WebSocket

在講Websocket之前,先了解下 long pollajax輪詢 的原理。

ajax輪詢

ajax輪詢的原理非常簡單,讓瀏覽器隔個幾秒就發送一次請求,詢問服務器是否有新信息。

long poll

long poll 其實原理跟 ajax輪詢 差不多,都是採用輪詢的方式,不過採取的是阻塞模型(一直打電話,沒收到就不掛電話),也就是說,客戶端發起連接後,如果沒消息,就一直不返回Response給客戶端。直到有消息才返回,返回完之後,客戶端再次建立連接,週而復始。

ajax輪詢 需要服務器有很快的處理速度和資源(速度)。long poll 需要有很高的併發,也就是說同時接待客戶的能力(場地大小)。

Websocket

WebSocket是一種在單個TCP連接上進行全雙工通訊的協議。WebSocket允許服務端主動向客戶端推送數據。在WebSocket協議中,客戶端瀏覽器和服務器只需要完成一次握手就可以創建持久性的連接,並在瀏覽器和服務器之間進行雙向的數據傳輸。[外鏈圖片轉存失敗(img-hQOZev9H-1565446193334)()]

WebSocket的請求頭中重要的字段:

  • ConnectionUpgrade:表示客戶端發起的WebSocket請求
  • Sec-WebSocket-Version:客戶端所使用的WebSocket協議版本號,服務端會確認是否支持該版本號
  • Sec-WebSocket-Key:一個Base64編碼值,由瀏覽器隨機生成,用於升級request

WebSocket的響應頭中重要的字段:

  • HTTP/1.1 101 Swi tching Protocols:切換協議,WebSocket協議通過HTTP協議來建立運輸層的TCP連接
  • ConnectionUpgrade:表示服務端發起的WebSocket響應
  • Sec-WebSocket-Accept:表示服務器接受了客戶端的請求,由Sec-WebSocket-Key計算得來

WebSocket協議的優點:

  • 支持雙向通信,實時性更強
  • 數據格式比較輕量,性能開銷小,通信高效
  • 支持擴展,用戶可以擴展協議或者實現自定義的子協議(比如支持自定義壓縮算法等)

WebSocket協議的優點:

  • 少部分瀏覽器不支持,瀏覽器支持的程度與方式有區別
  • 長連接對後端處理業務的代碼穩定性要求更高,後端推送功能相對複雜
  • 成熟的HTTP生態下有大量的組件可以複用,WebSocket較少

WebSocket的應用場景:

  • 即時聊天通信,網站消息通知
  • 在線協同編輯,如騰訊文檔
  • 多玩家在線遊戲,視頻彈幕,股票基金實施報價

Channels

Django本身不支持WebSocket,但可以通過集成Channels框架來實現WebSocket

Channels是針對Django項目的一個增強框架,可以使Django不僅支持HTTP協議,還能支持WebSocketMQTT等多種協議,同時Channels還整合了Djangoauth以及session系統方便進行用戶管理及認證。

[外鏈圖片轉存失敗(img-rV2ydJ2h-1565446193339)()]

channels中文件和配置的含義

  • asgi.py:介於網絡協議服務和Python應用之間的接口,能夠處理多種通用協議類型,包括HTTPHTTP2WebSocket
  • channel_layers:在settings.py中配置。類似於一個通道,發送者(producer)在一段發送消息,消費者(consumer)在另一端進行監聽
  • routings.py:相當於Django中的urls.py
  • consumers.py:相當於Django中的views.py

WSGI

WSGI(Python Web Server Gateway Interface):爲Python語言定義的Web服務器和Web應用程序或者框架之間的一種簡單而通用的接口。

ASGI

ASGI(Asynchronous Web Server Gateway Interface):異步網關協議接口,一個介於網絡協議服務和Python應用之間的標準接口,能夠處理多種通用的協議類型,包括HTTPHTTP2WebSocket

WSGI是基於HTTP協議模式的,不支持WebSocket,而ASGI的誕生則是爲了解決Python常用的WSGI不支持當前Web開發中的一些新的協議標準。同時,ASGI對於WSGI原有的模式的支持和WebSocket的擴展,即ASGIWSGI的擴展。

Django中使用

1、安裝channels,要注意版本的對應,在channels官網中可以得到對應的django版本

pip install channels==2.1.7

2、修改settings.py文件,

# APPS中添加channels
INSTALLED_APPS = [
    'django.contrib.staticfiles',
    ... ...
    'channels',
]
# 指定ASGI的路由地址
ASGI_APPLICATION = 'webapp.routing.application' #ASGI_APPLICATION 指定主路由的位置爲webapp下的routing.py文件中的application

3、setting.py的同級目錄下創建routing.py路由文件,routing.py類似於Django中的url.py指明websocket協議的路由

from channels.auth import AuthMiddlewareStack
from channels.routing import ProtocolTypeRouter, URLRouter
import chat.routing
# 第一種設置的方法
from channels.security.websocket import AllowedHostsOriginValidator
application = ProtocolTypeRouter({
    # 普通的HTTP協議在這裏不需要寫,框架會自己指明
    'websocket': AllowedHostsOriginValidator(
	AuthMiddlewareStack(
    	URLRouter(
        	# 指定去對應應用的routing中去找路由
        	chat.routing.websocket_urlpatterns
    		)
		),
	)
})

# 第二種設置的方法,需要手動指定可以訪問的IP
from channels.security.websocket import OriginValidator
application = ProtocolTypeRouter({
    # 普通的HTTP協議在這裏不需要寫,框架會自己指明
    'websocket': OriginValidator(
	AuthMiddlewareStack(
    	URLRouter(
        	# 指定去對應應用的routing中去找路由
        	chat.routing.websocket_urlpatterns
    		)
		),
        # 設置可以訪問的IP列表
        ['*']
	)
})

ProtocolTypeRouterASIG支持多種不同的協議,在這裏可以指定特定協議的路由信息,我們只使用了websocket協議,這裏只配置websocket即可

AllowedHostsOriginValidator:指定允許訪問的IP,設置後會去Django中的settings.py中去查找ALLOWED_HOSTS設置的IP

AuthMiddlewareStack:用於WebSocket認證,繼承了Cookie MiddlewareSessionMiddleware,SessionMiddleware。djangochannels封裝了djangoauth模塊,使用這個配置我們就可以在consumer中通過下邊的代碼獲取到用戶的信息

def connect(self):
    self.user = self.scope["user"]

self.scope類似於django中的request,包含了請求的type、path、header、cookie、session、user等等有用的信息

URLRouter: 指定路由文件的路徑,也可以直接將路由信息寫在這裏,代碼中配置了路由文件的路徑,會去對應應用下的routeing.py文件中查找websocket_urlpatterns

chat/routing.py內容如下

from django.urls import path
from chat.consumers import ChatConsumer

websocket_urlpatterns = [
    path('ws/chat/', EchoConsumer), # 這裏可以定義自己的路由
    path('ws/<str:username>/',MessagesConsumer) # 如果是傳參的路由在連接中獲取關鍵字參數方法:self.scope['url_route']['kwargs']['username']
]

routing.py路由文件跟djangourl.py功能類似,語法也一樣,意思就是訪問ws/chat/都交給ChatConsumer處理。

4、在要使用WebSocket的應用中創建consumers.pyconsumers.py是用來開發ASGI接口規範的python應用,而Django中的view.py是用來開發符合WSGI接口規範的python應用。

首先了解下面的意思:
event loop事件循環、event handler事件處理器、sync同步、async異步

下面是一個同步的consumers.py

from channels.consumer import SyncConsumer

class EchoConsumer(SyncConsumer):
    def websocket_connect(self, event):
        self.send({
            'type': "websocket.accept"  # 這裏是固定的寫法,type不可以改變,是ASGI的接口規範,
        })

    def websocket_receive(self, event):
        user = self.scope['user'] # 獲取當前用戶,沒有登錄顯示匿名用戶
        path = self.scope['path'] # Request請求的路徑,HTTP,WebSocket
        
        # ORM 同步代碼 假如要查詢數據庫
        user = User.objects.filter(username=username)
        self.send({
            "type": "websocket.send",  # 這裏是固定的寫法,type不可以改變
            "text": event['text']  # 把前端返回過來的text返回回去
        })

異步的consumers.py

from channels.consumer import AsyncConsumer

class EchoConsumer(AsyncConsumer):
    async def websocket_connect(self, event):
        await self.send({
            'type': "websocket.accept"
        })
    async def websocket_receive(self, event):
        # 在異步中所有的操作都需要異步執行,比如發送請求,操作ORM
        # 對於異步的請求可以使用模塊aiohttp實現異步的request請求
        # ORM 異步代碼 假如要查詢數據庫
        # 第一種方式 使用channels通過的模塊
        from channels.db import database_sync_to_async
        user = await database_sync_to_async(User.objects.filter(username=username))
        # 第二種方式 使用裝飾器
        @database_sync_to_async
        def get_username():
			return User.objects.filter(username=username)
        await self.send({
            "type": "websocket.send", 
            "text": event['text']  
        })

需要注意的是在異步中所有的邏輯都應該是異步的,不可以那同步的和異步的代碼混合使用。

繼承WebSocketConsumer的連接

from channels.generic.websocket import AsyncWebsocketConsumer

class MessageConsumer(AsyncWebsocketConsumer):
    def connect(self):
        if self.scope['user'].is_anonymous:
            # 沒有登陸的用戶直接斷開連接
            self.close()
        else:
            # 加入聊天組,並監聽對應的頻道
            # self.channel_layer進行監聽頻道
            # self.scope['user'].username以用戶名作爲組名,
            # self.channel_name 要進行監聽的頻道,會自己生成唯一的頻道
            self.channel_layer.group_add(self.scope['user'].username,self.channel_name)
            self.accept()

    def receive(self, text_data=None, bytes_data=None):
        '''接受私信'''
        self.send(text_data=json.dumps(text_data)) # 將接收到的信息返回出去
    def disconnect(self, code):
        '''離開聊天組'''
        # self.scope['user'].username要結束的組名
     	self.channel_layer.group_discard(self.scope['user'].username,self.channel_name)

要改爲異步和前面的方法一致

信息交互的週期

在這裏插入圖片描述

項目中可以在視圖中直接推送信息給用戶

view.py
from channels.layers import get_channel_layer
def send_message(request):
	... ...
	channel_layer = get_channel_layer()
	payload = {
        'type':'receive', # 這裏的寫法是固定的,receive代表的是consumers中的receive函數
        'message':'要發送的信息',
        'sender':sender.username, # 發送者的暱稱
	}
	channel_layer.group_send(receiver_username,payload)

前端實現WebSocket

WebSocket對象一個支持四個消息:onopenonmessageoncluseonerror,我們這裏用了兩個onmessage和onclose

onopen: 當瀏覽器和websocket服務端連接成功後會觸發onopen消息

onerror: 如果連接失敗,或者發送、接收數據失敗,或者數據處理出錯都會觸發onerror消息

onmessage: 當瀏覽器接收到websocket服務器發送過來的數據時,就會觸發onmessage消息,參數e包含了服務端發送過來的數據

onclose: 當瀏覽器接收到websocket服務器發送過來的關閉連接請求時,會觸發onclose消息載請註明出處。

% extends "base.html" %}

{% block content %}
  <textarea class="form-control" id="chat-log" disabled rows="20"></textarea><br/>
  <input class="form-control" id="chat-message-input" type="text"/><br/>
  <input class="btn btn-success btn-block" id="chat-message-submit" type="button" value="Send"/>
{% endblock %}

{% block js %}
<script>
  var chatSocket = new WebSocket(
    'ws://' + window.location.host + '/ws/chat/');

  chatSocket.onmessage = function(e) {
    var data = JSON.parse(e.data);
    var message = data['message'];
    document.querySelector('#chat-log').value += (message + '\n');
  };

  chatSocket.onclose = function(e) {
    console.error('Chat socket closed unexpectedly');
  };

  document.querySelector('#chat-message-input').focus();
  document.querySelector('#chat-message-input').onkeyup = function(e) {
    if (e.keyCode === 13) {  // enter, return
        document.querySelector('#chat-message-submit').click();
    }
  };

  document.querySelector('#chat-message-submit').onclick = function(e) {
    var messageInputDom = document.querySelector('#chat-message-input');
    var message = messageInputDom.value;
    chatSocket.send(JSON.stringify({
        'message': message
    }));

    messageInputDom.value = '';
  };
</script>
{% endblock %}

參考博客:https://juejin.im/post/5cb67fc3e51d456e6a1d0237

前後端分離項目實現Websocket

環境版本:
	django==2.0
	channels==2.2.0
	channels-redis==2.3.2

vue實現代碼:

全局配置 websocket.js

const path = window.location.host
const WSS_URL = 'wss://' + path + '/ws/chat/'
let Socket = ''
let setIntervalWebsocketPush = null

/** 建立連接 */
export function createSocket(projectId) {
  if (!Socket) {
    console.log('建立websocket連接')
    Socket = new WebSocket(WSS_URL + projectId)
    Socket.onopen = onopenWS
    Socket.onmessage = onmessageWS
    Socket.onerror = onerrorWS
    Socket.onclose = oncloseWS
  } else {
    console.log('websocket已連接')
  }
}
/** 打開WS之後發送心跳 */
export function onopenWS() {
  sendPing() // 發送心跳
}
/** 連接失敗重連 */
export function onerrorWS() {
  clearInterval(setIntervalWebsocketPush)
  Socket.close()
  createSocket() // 重連
}
/** WS數據接收統一處理 */
export function onmessageWS(e) {
  window.dispatchEvent(new CustomEvent('onmessageWS', {
    detail: e.data
  }))
}
/** 發送數據 */
export function sendWSPush(eventTypeArr) {
  const obj = {
    appId: 'airShip',
    cover: 0,
    event: eventTypeArr
  }
  if (Socket !== null && Socket.readyState === 3) {
    Socket.close()
    createSocket() // 重連
  } else if (Socket.readyState === 1) {
    Socket.send(JSON.stringify(obj))
  } else if (Socket.readyState === 0) {
    setTimeout(() => {
      Socket.send(JSON.stringify(obj))
    }, 3000)
  }
}
/** 關閉WS */
export function oncloseWS() {
  clearInterval(setIntervalWebsocketPush) // 取消由setInterval()設置的timeout。
  Socket = ''
  console.log('websocket已斷開')
}
/** 發送心跳 */
export function sendPing() {
  Socket.send('ping')
  setIntervalWebsocketPush = setInterval(() => {
    Socket.send('ping')
  }, 5000)
}

組件內使用 Index.vue

import { createSocket } from '@/api/websocket'
destroyed() {
	// 根據需要,銷燬事件監聽
	window.removeEventListener('onmessageWS', this.getDataFunc)
},
created() {
	createSocket(projectId)
	// 添加事件監聽
	window.addEventListener('onmessageWS', this.getDataFunc)
},
methods:{
	// 監聽ws數據響應
    getDataFunc(e) {
      const tempData = JSON.parse(e.detail)
    }
}

django實現代碼:

settings.py

# 在應用中註冊 channels
# Channels
ASGI_APPLICATION = 'cmdb.routing.application'
CHANNEL_LAYERS = {
    "default": {
        "BACKEND": "channels_redis.core.RedisChannelLayer",
        "CONFIG": {
            "hosts": [('127.0.0.1', 6379)],
        },
    },
}

consumers.py

import json
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync

class MsgConsumer(WebsocketConsumer):
    def __init__(self, *args, **kwargs):
        self.room_group_name = ""
        super(MsgConsumer, self).__init__(*args, **kwargs)

    def connect(self):
        # 鏈接後將應用id作爲組名,
        project_id = self.scope["path_remaining"]
        self.room_group_name = project_id
        async_to_sync(self.channel_layer.group_add)(
            self.room_group_name,
            self.channel_name
        )
        self.accept()

    def disconnect(self, close_code):
        # 斷開連接時從組裏面刪除
        async_to_sync(self.channel_layer.group_discard)(
            self.room_group_name,
            self.channel_name
        )

    def receive(self, text_data=None, bytes_data=None):
        # 接受到信息時執行
        text_data_json = json.loads(text_data)
        message = text_data_json['message']
        async_to_sync(self.channel_layer.group_send)(
            self.room_group_name, {
                'type': 'chat.message',  # 必須在MsgConsumer類中定義chat_message
                'message': message
            })

    def send_message(self, event): 
        # 發送信息是執行
        message = event['message']
        self.send(text_data=json.dumps({
            'message': message
        }))

    def chat_message(self, event):
        message = event['message']
        self.send(text_data=json.dumps({
            'message': message
        }))

在其他視圖內使用

view.py

# 測試函數
def send_fun():
    from asgiref.sync import async_to_sync
    from channels.layers import get_channel_layer
    channels_layer = get_channel_layer()
    data = '我是追加的內容\n'
    for i in [0, 1, 2, 3, 4, 5, 6]:
        send_dic = {
            "type": "send.message",
            "message": {
                'step': i,
                'content': data
            }
        }
        if i < 5:
            import time
            for j in range(19):
                time.sleep(0.5)
                send_dic = {
                    "type": "send.message", # 必須在MsgConsumer類中定義send_message
                    "message": {
                        'step': i,
                        'content': data
                    }
                }
                time.sleep(0.5)
                async_to_sync(channels_layer.group_send)(room_group_name , send_dic)
        else:
            async_to_sync(channels_layer.group_send)(room_group_name , send_dic)
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章