文章目錄
WebSocket
在講Websocket
之前,先了解下 long poll
和 ajax輪詢
的原理。
ajax輪詢
ajax輪詢的原理非常簡單,讓瀏覽器隔個幾秒就發送一次請求,詢問服務器是否有新信息。
long poll
long poll
其實原理跟 ajax
輪詢 差不多,都是採用輪詢的方式,不過採取的是阻塞模型(一直打電話,沒收到就不掛電話),也就是說,客戶端發起連接後,如果沒消息,就一直不返回Response給客戶端。直到有消息才返回,返回完之後,客戶端再次建立連接,週而復始。
ajax
輪詢 需要服務器有很快的處理速度和資源(速度)。long poll
需要有很高的併發,也就是說同時接待客戶的能力(場地大小)。
Websocket
WebSocket
是一種在單個TCP
連接上進行全雙工通訊的協議。WebSocket
允許服務端主動向客戶端推送數據。在WebSocket
協議中,客戶端瀏覽器和服務器只需要完成一次握手就可以創建持久性的連接,並在瀏覽器和服務器之間進行雙向的數據傳輸。
WebSocket
的請求頭中重要的字段:
Connection
和Upgrade
:表示客戶端發起的WebSocket
請求Sec-WebSocket-Version
:客戶端所使用的WebSocket
協議版本號,服務端會確認是否支持該版本號Sec-WebSocket-Key
:一個Base64
編碼值,由瀏覽器隨機生成,用於升級request
WebSocket
的響應頭中重要的字段:
HTTP/1.1 101 Swi tching Protocols
:切換協議,WebSocket
協議通過HTTP
協議來建立運輸層的TCP
連接Connection
和Upgrade
:表示服務端發起的WebSocket
響應Sec-WebSocket-Accept
:表示服務器接受了客戶端的請求,由Sec-WebSocket-Key
計算得來
WebSocket
協議的優點:
- 支持雙向通信,實時性更強
- 數據格式比較輕量,性能開銷小,通信高效
- 支持擴展,用戶可以擴展協議或者實現自定義的子協議(比如支持自定義壓縮算法等)
WebSocket
協議的優點:
- 少部分瀏覽器不支持,瀏覽器支持的程度與方式有區別
- 長連接對後端處理業務的代碼穩定性要求更高,後端推送功能相對複雜
- 成熟的
HTTP
生態下有大量的組件可以複用,WebSocket
較少
WebSocket
的應用場景:
- 即時聊天通信,網站消息通知
- 在線協同編輯,如騰訊文檔
- 多玩家在線遊戲,視頻彈幕,股票基金實施報價
Channels
Django
本身不支持WebSocket
,但可以通過集成Channels
框架來實現WebSocket
Channels
是針對Django項目的一個增強框架,可以使Django
不僅支持HTTP
協議,還能支持WebSocket
,MQTT
等多種協議,同時Channels
還整合了Django
的auth
以及session
系統方便進行用戶管理及認證。
channels中文件和配置的含義
asgi.py
:介於網絡協議服務和Python
應用之間的接口,能夠處理多種通用協議類型,包括HTTP
、HTTP2
和WebSocket
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
應用之間的標準接口,能夠處理多種通用的協議類型,包括HTTP
,HTTP2
和WebSocket
。
WSGI
是基於HTTP
協議模式的,不支持WebSocket
,而ASGI
的誕生則是爲了解決Python
常用的WSGI
不支持當前Web
開發中的一些新的協議標準。同時,ASGI
對於WSGI
原有的模式的支持和WebSocket
的擴展,即ASGI
是WSGI
的擴展。
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列表
['*']
)
})
ProtocolTypeRouter:ASIG
支持多種不同的協議,在這裏可以指定特定協議的路由信息,我們只使用了websocket
協議,這裏只配置websocket
即可
AllowedHostsOriginValidator:指定允許訪問的IP
,設置後會去Django
中的settings.py
中去查找ALLOWED_HOSTS
設置的IP
AuthMiddlewareStack:用於WebSocket
認證,繼承了Cookie Middleware
,SessionMiddleware,
SessionMiddleware。django
的channels
封裝了django
的auth
模塊,使用這個配置我們就可以在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
路由文件跟django
的url.py
功能類似,語法也一樣,意思就是訪問ws/chat/
都交給ChatConsumer
處理。
4、在要使用WebSocket的應用中創建consumers.py
,consumers.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
對象一個支持四個消息:onopen
,onmessage
,oncluse
和onerror
,我們這裏用了兩個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)