websocket,web版的socket
原web中:
- http協議,無狀態&短連結
- 客戶端主動連線伺服器
- 客戶端向伺服器傳送訊息,伺服器收到返回資料
- 客戶端收到資料
- 斷開連線
- https一些 + 對資料進行加密。
我們在開發過程中想要保留一些狀態資訊,基於cookie來做
現在支援:
-
http協議。一次請求一次響應。
-
websocket協議,建立持久的連線不斷開,基於這個連線可以進行收發資料。【服務端向客戶端主動推送訊息】
- web聊天室
- 實時圖表格,柱狀圖,餅圖(highcharts)
websocket原理
- http協議
- 連線
- 資料傳輸
- 埠連線
- webscoket協議,是建立在http協議之上的
- 連線,客戶端發起
- 握手(驗證),客戶端傳送一個訊息,後端接收到訊息再做一些特殊處理並返回。服務端支援websocket。
- 收發資料(加密)
- 斷開連線。
django框架
django預設不支援websocket,需要安裝元件:
pip install channels
pip install daphne
配置
-
註冊channels, daphne 必須在 INSTALLED_APPS 中的 django.contrib.staticfiles 之前,所以建議這兩個app註冊到其他之前,daphne又需要在channels之前。
INSTALLED_APPS = [ "daphne", # 註冊 "channels", # 註冊 "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", ]
-
在settings.py中新增asgi_application
ASGI_APPLICATION = "專案名.asgi.application" # django3會自動生成application在asgi.py模組中
-
修改asgi.py檔案
import os from django.core.asgi import get_asgi_application from channels.routing import ProtocolTypeRouter, URLRouter from . import routing os.environ.setdefault("DJANGO_SETTINGS_MODULE", "ws_demo.settings") # application = get_asgi_application() 預設的 application = ProtocolTypeRouter({ "http": get_asgi_application(), # 自動走urls.py 找檢視 就是之前正常的流程 "websocket": URLRouter(routing.websocket_urlpatterns), # 建立的routing就相當於之前的urls.py 建立的consumer.py 就相當於之前的views.py })
-
在settings.py的同級目錄建立routing.py
from django.urls import re_path, path from app01 import consumers websocket_urlpatterns = [ # re_path(r"ws/(?P<group>\w+)/$", consumers.ChatConsumer.as_asgi()), path("ws/", consumers.ChatConsumer.as_asgi()) ]
-
在app01(就是你的app)目錄下建立consumers.py,編寫處理websocket的業務邏輯。
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): # 有客戶端向後端傳送websocket連線的請求時,自動觸發。 def websocket_connect(self, message): # 服務端允許客戶端創立連線 self.accept() # 瀏覽器基於websocket向後端傳送資料,自動觸發接收訊息。 def websocket_receive(self, message): print(message) self.send("不要回復訊息 不要") # self.close() 服務端也可以主動斷開連線, self.close() # 客戶端主動埠連線時,會自動觸發 def websocket_disconnect(self, message): print("連線已斷開") raise StopConsumer()
在django中需要了解的
wsgi,之前預設使用的都是wsgi
asgi,等於wsgi + 非同步 + websocket
http
urls.py
views.py
websocket
routings.py
consumers.py
聊天室(單個人)
-
訪問地址看到聊天室的頁面,HTTP請求。
-
讓客戶端主動向服務端發起websocket連線,服務端接受到連線後透過(握手)。
-
客戶端:
const socket = new WebSocket("ws://127.0.0.1:8888/ws/"); // 必須以ws:開頭 ws結尾
-
服務端
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): # 有客戶端向後端傳送websocket連線的請求時,自動觸發。 def websocket_connect(self, message): print('收到請求了') # 服務端允許客戶端創立連線 self.accept()
-
-
收發訊息
- 客戶端
socket.onopen = (event)=>{ socket.send("hello server!") };
- 服務端
class ChatConsumer(WebsocketConsumer):
# 上面的程式碼和之前一樣
def websocket_receive(self, message):
# message是一個字典 {'type': 'websocket.receive', 'text': 'hello server!'}
print("收到客戶端的訊息", message.get('text')) # hello server!
# 服務端給客戶端發訊息
self.send("不要回復訊息 不要")
# self.close() 服務端也可以主動斷開連線, self.close()
```
-
收發訊息(服務端主動發給客戶端)
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): # 有客戶端向後端傳送websocket連線的請求時,自動觸發。 def websocket_connect(self, message): # 服務端允許客戶端創立連線 self.accept() # 連線建立後 服務端主動給客戶端傳送訊息 self.send('客官您來啦~') # self 代表連線物件
-
斷開連線(客戶端主動斷開)
-
客戶端
socket.close()
-
服務端
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): # 其他程式碼一樣 # 客戶端主動埠連線時,會自動觸發 def websocket_disconnect(self, message): print("連線已斷開") # 這個異常表示同意斷開 raise StopConsumer()
-
斷開連線(客戶端傳送關鍵字,服務端收到後先斷開服務端,然後觸發客戶端的回撥函式onclose)
-
客戶端
socket.send('關閉')
-
服務端
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer class ChatConsumer(WebsocketConsumer): # 其他程式碼一樣 def websocket_receive(self, message): text = message['text'] if text == '關閉': self.close() # 執行這個會觸發客戶端的 socket.onclose()回撥函式,也會執行服務端自己的斷開websocket_disconnect,總結就是隻要客戶端和服務端誰執行了close,整個連線就都斷開了。
-
客戶端
// 服務端主動斷開連線的時候,該方法會被觸發 socket.onclose = (event)=>{ console.log("連結已關閉"); }
-
補充
# 如果想要服務端主動斷開連線的時候,不觸發websocket_disconnect,那麼在服務端斷開之後,raise StopConsumer()即可 def websocket_receive(self, message): text = message['text'] if text == '關閉': self.close() # 執行這個會觸發客戶端的 socket.onclose()回撥函式 raise StopConsumer() self.send(text + "謝謝!") # 客戶端主動埠連線時,會自動觸發,也就是這裡只觸發客戶端斷開的邏輯,而不是傳送關鍵字讓服務端斷開的邏輯。 def websocket_disconnect(self, message): print("連線已斷開") raise StopConsumer()
-
-
程式碼整合
# 後端
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
self.accept()
def websocket_receive(self, message):
text = message['text']
if text == '關閉':
self.close()
raise StopConsumer()
self.send(text + "謝謝!")
def websocket_disconnect(self, message):
print("連線已斷開")
raise StopConsumer()
// 前端
const socket = new WebSocket("ws://127.0.0.1:8888/ws/");
// 建立好連線自動觸發(服務端self.accept())後自動觸發。
socket.onopen = (event)=>{
this.isShow = true
// 往服務端傳送訊息
socket.send(this.msg)
};
// 當websocket收到服務端的訊息時,會自動觸發這個函式
// 資料放在event裡面,透過event.data能獲取到資料的內容
socket.onmessage = (event)=>{
this.messageList.push({
id: this.messageList.length + 1,
data: event.data
})
};
// 如果有錯誤
socket.onerror = (error)=>{
console.log("websocket error:", error);
};
// 服務端主動斷開連時,這個方法會觸發
socket.onclose = (event)=>{
swal('服務端已斷開連結')
console.log("websocket connection closed:", event);
}
},
小結:
基於django實現websocket請求,但只能對某個人進行處理。主要就是後端的self
聊天室(群聊一)
修改後端即可
from channels.generic.websocket import WebsocketConsumer
from channels.exceptions import StopConsumer
# 1. 建立一個列表,儲存全部連線物件
# 因為每一個self都是一個連線物件,所以把他接入到一個列表中
CONN_LIST= []
class ChatConsumer(WebsocketConsumer):
def websocket_connect(self, message):
self.accept()
CONN_LIST.append(self) # 把連線物件加入到列表
def websocket_receive(self, message):
text = message['text']
for conn in CONN_LIST: # 迴圈遍歷列表,拿到每一個連線物件,給每一個連線物件發訊息
conn.send(text + "訊息已收到,這是自動回覆。")
def websocket_disconnect(self, message):
CONN_LIST.remove(self) # 當連線斷開後,把這個連線物件送物件列表中移除
raise StopConsumer()
前端程式碼
<template>
<div class="home">
<div class="content">
<p v-show="isShow">[連結建立成功]</p>
<hr v-show="isShow">
<ul v-for="(item, index) in messageList" :key="item.id">
<li>收到來自服務端的訊息:{{ index }}-{{ item.data }}</li>
</ul>
</div>
<div class="inner">
<label for="message">請輸入:</label>
<el-input id="message" v-model="msg"></el-input>
<p>
<el-row>
<el-button type="success" @click="sendWS">傳送</el-button>
<el-button type="danger" @click="closeConn">關閉連線</el-button>
<el-button type="warning" @click="messageList=[]">清空訊息</el-button>
</el-row>
</p>
</div>
</div>
</template>
<script>
export default {
data() {
return {
msg: '',
isShow: false,
messageList: []
}
},
methods: {
sendWS(){
const socket = new WebSocket("ws://127.0.0.1:8888/");
// 建立好連線自動觸發(服務端self.accept())後自動觸發。
socket.onopen = (event)=>{
this.isShow = true
// 往服務端傳送訊息
socket.send(this.msg)
};
// 當websocket收到服務端的訊息時,會自動觸發這個函式
// 資料放在event裡面,透過event.data能獲取到資料的內容
socket.onmessage = (event)=>{
this.messageList.push({
id: this.messageList.length + 1,
data: event.data
})
};
// 如果有錯誤
socket.onerror = (error)=>{
console.log("websocket error:", error);
};
// 服務端主動斷開連時,這個方法會觸發
socket.onclose = (event)=>{
swal('服務端已斷開連結')
console.log("websocket connection closed:", event);
}
},
// 關閉連線函式
closeConn(){
swal("關閉連線函式已觸發")
socket.close()
}
},
created(){
let token = sessionStorage.getItem("token")
if (!token){
swal('請先登入').then(()=>{
this.$router.push("/login")
})
}
}
}
</script>
<style scoped>
.home .content {
border: 1px solid #dddddd;
width: 800px;
height: 500px;
margin: 50px auto;
overflow: auto;
}
.home .inner {
width: 800px;
margin: 50px auto;
}
</style>
聊天室(群聊二)
基於channels中提供的channel layers來實現
-
settings中配置
CHANNEL_LAYERS = { "default": { "BACKEND": "channels.layers.InMemoryChannelLayer" } }
如果想要換成redis的
pip3 install channels-redis
# 寫法1 CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": [('IP', 6379)] }, }, } # 寫法2 CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': {"hosts": ["redis://IP:6379/1"],}, }, } # 寫法3 CHANNEL_LAYERS = { "default": { "BACKEND": "channels_redis.core.RedisChannelLayer", "CONFIG": { "hosts": ["redis://:password@IP:6379/0"], "symmetric_encryption_keys": [SECRET_KEY], }, }, }
-
consumers中新增特殊程式碼
from channels.generic.websocket import WebsocketConsumer from channels.exceptions import StopConsumer
from asgiref.sync import async_to_sync
class ChatConsumer(WebsocketConsumer):
# 有客戶端向後端傳送websocket連線的請求時,自動觸發。
def websocket_connect(self, message):
# 服務端允許客戶端創立連線
self.accept()
# 將這個客戶端的連線物件加入到某個地方(內容 or redis)
# xxx 表示一個群名,隨便寫 slef.channel_name 表示一個隨機的別名
# 需要將非同步轉成同步,用同步的方式做
self.group = self.scope['url_route']['kwargs']['room_id']
async_to_sync(self.channel_layer.group_add)(self.group, self.channel_name)
# 瀏覽器基於websocket向後端傳送資料,自動觸發接收訊息。
def websocket_receive(self, message):
text = message['text']
if text == '關閉':
self.close() # 執行這個會觸發客戶端的 socket.onclose()回撥函式
raise StopConsumer()
return
# 通知組內的所有客戶端,執行xx_oo 方法,在此方法中可以自己取定義任意的功能,只是在欄位的值中需要寫成 xx.oo
async_to_sync(self.channel_layer.group_send)(self.group, {"type":"xx.oo", "message": message})
def xx_oo(self, event):
text = event["message"]['text']
# 這裡的send表示給組裡的每一個人send不是單獨發哪一個,就是群聊裡面發
self.send(text + "噠咩")
# 客戶端主動埠連線時,會自動觸發
def websocket_disconnect(self, message):
print("連線已斷開")
# 連線斷開,將這個連線從組裡移除掉
async_to_sync(self.channel_layer.group_discard)(self.group, self.channel_name)
raise StopConsumer()
```
-
routing的路徑為
from django.urls import path from app01 import consumers websocket_urlpatterns = [ path("chat/<str:room_id>/", consumers.ChatConsumer.as_asgi())
-
前端訪問的地址為
const socket = new WebSocket("ws://127.0.0.1:8888/chat/123/");
簡單總結
- websocket是什麼?協議。
- django中實現websocket,channels元件。
- 單獨連線和收發資料
- 手動建立連線表 & channel layers