WebSocket - 開啓通往新世界的大門html
WebSocket是什麼?前端
WebSocket是一種在單個TCP鏈接上進行全雙工通信的協議。WebSocket容許服務端主動向客戶端推送數據。在WebSocket協議中,客戶端瀏覽器和服務器只須要完成一次握手就能夠建立持久性的鏈接,並在瀏覽器和服務器之間進行雙向的數據傳輸。python
WebSocket有什麼用?git
WebSocket區別於HTTP協議的一個最爲顯著的特色是,WebSocket協議能夠由服務端主動發起消息,對於瀏覽器須要及時接收數據變化的場景很是適合,例如在Django中遇到一些耗時較長的任務咱們一般會使用Celery來異步執行,那麼瀏覽器若是想要獲取這個任務的執行狀態,在HTTP協議中只能經過輪訓的方式由瀏覽器不斷的發送請求給服務器來獲取最新狀態,這樣發送不少無用的請求不只浪費資源,還不夠優雅,若是使用WebSokcet來實現就很完美了github
WebSocket的另一個應用場景就是下文要說的聊天室,一個用戶(瀏覽器)發送的消息須要實時的讓其餘用戶(瀏覽器)接收,這在HTTP協議下是很難實現的,但WebSocket基於長鏈接加上能夠主動給瀏覽器發消息的特性處理起來就遊刃有餘了web
初步瞭解WebSocket以後,咱們看看如何在Django中實現WebSocketredis
Django自己不支持WebSocket,但能夠經過集成Channels框架來實現WebSocketshell
Channels是針對Django項目的一個加強框架,可使Django不只支持HTTP協議,還能支持WebSocket,MQTT等多種協議,同時Channels還整合了Django的auth以及session系統方便進行用戶管理及認證。django
我下文全部的代碼實現使用如下python和Django版本json
我假設你已經新建了一個django項目,項目名字就叫webapp
,目錄結構以下
project
- webapp
- __init__.py
- settings.py
- urls.py
- wsgi.py
- manage.py
複製代碼
pip install channels==2.1.7
複製代碼
# APPS中添加channels INSTALLED_APPS = [ 'django.contrib.staticfiles', 'channels', ] # 指定ASGI的路由地址 ASGI_APPLICATION = 'webapp.routing.application' 複製代碼
channels運行於ASGI協議上,ASGI的全名是Asynchronous Server Gateway Interface。它是區別於Django使用的WSGI協議 的一種異步服務網關接口協議,正是由於它才實現了websocket
ASGI_APPLICATION 指定主路由的位置爲webapp下的routing.py文件中的application
from channels.routing import ProtocolTypeRouter application = ProtocolTypeRouter({ # 暫時爲空,下文填充 }) 複製代碼
C:\python36\python.exe D:/demo/tailf/manage.py runserver 0.0.0.0:80 Performing system checks... Watching for file changes with StatReloader System check identified no issues (0 silenced). April 12, 2019 - 17:44:52 Django version 2.2, using settings 'webapp.settings' Starting ASGI/Channels version 2.1.7 development server at http://0.0.0.0:80/ Quit the server with CTRL-BREAK. 複製代碼
仔細觀察上邊的輸出會發現Django啓動中的Starting development server
已經變成了Starting ASGI/Channels version 2.1.7 development server
,這代表項目已經由django使用的WSGI協議轉換爲了Channels使用的ASGI協議
至此Django已經基本集成了Channels框架
上邊雖然在項目中集成了Channels,但並無任何的應用使用它,接下來咱們以聊天室的例子來說解Channels的使用
假設你已經建立好了一個叫chat的app,並添加到了settings.py的INSTALLED_APPS中,app的目錄結構大概以下
chat
- migrations
- __init__.py
- __init__.py
- admin.py
- apps.py
- models.py
- tests.py
- views.py
複製代碼
咱們構建一個標準的Django聊天頁面,相關代碼以下
url:
from django.urls import path from chat.views import chat urlpatterns = [ path('chat', chat, name='chat-url') ] 複製代碼
view:
from django.shortcuts import render def chat(request): return render(request, 'chat/index.html') 複製代碼
template:
{% 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 %} 複製代碼
經過上邊的代碼一個簡單的web聊天頁面構建完成了,訪問頁面大概樣子以下:
接下來咱們利用Channels的WebSocket協議實現消息的發送接收功能
from channels.auth import AuthMiddlewareStack from channels.routing import ProtocolTypeRouter, URLRouter import chat.routing application = ProtocolTypeRouter({ 'websocket': AuthMiddlewareStack( URLRouter( chat.routing.websocket_urlpatterns ) ), }) 複製代碼
ProtocolTypeRouter: ASIG支持多種不一樣的協議,在這裏能夠指定特定協議的路由信息,咱們只使用了websocket協議,這裏只配置websocket便可
AuthMiddlewareStack: django的channels封裝了django的auth模塊,使用這個配置咱們就能夠在consumer中經過下邊的代碼獲取到用戶的信息
def connect(self): self.user = self.scope["user"] 複製代碼
self.scope
相似於django中的request,包含了請求的type、path、header、cookie、session、user等等有用的信息
URLRouter: 指定路由文件的路徑,也能夠直接將路由信息寫在這裏,代碼中配置了路由文件的路徑,會去chat下的routeing.py文件中查找websocket_urlpatterns,chat/routing.py
內容以下
from django.urls import path from chat.consumers import ChatConsumer websocket_urlpatterns = [ path('ws/chat/', ChatConsumer), ] 複製代碼
routing.py路由文件跟django的url.py功能相似,語法也同樣,意思就是訪問ws/chat/
都交給ChatConsumer
處理
from channels.generic.websocket import WebsocketConsumer import json class ChatConsumer(WebsocketConsumer): def connect(self): self.accept() def disconnect(self, close_code): pass def receive(self, text_data): text_data_json = json.loads(text_data) message = '運維咖啡吧:' + text_data_json['message'] self.send(text_data=json.dumps({ 'message': message })) 複製代碼
這裏是個最簡單的同步websocket consumer類,connect方法在鏈接創建時觸發,disconnect在鏈接關閉時觸發,receive方法會在收到消息後觸發。整個ChatConsumer類會將全部收到的消息加上「運維咖啡吧:」的前綴發送給客戶端
{% 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 %} 複製代碼
WebSocket對象一個支持四個消息:onopen,onmessage,oncluse和onerror,咱們這裏用了兩個onmessage和onclose
onopen: 當瀏覽器和websocket服務端鏈接成功後會觸發onopen消息
onerror: 若是鏈接失敗,或者發送、接收數據失敗,或者數據處理出錯都會觸發onerror消息
onmessage: 當瀏覽器接收到websocket服務器發送過來的數據時,就會觸發onmessage消息,參數e
包含了服務端發送過來的數據
onclose: 當瀏覽器接收到websocket服務器發送過來的關閉鏈接請求時,會觸發onclose消息
上邊的例子咱們已經實現了消息的發送和接收,但既然是聊天室,確定要支持多人同時聊天的,當咱們打開多個瀏覽器分別輸入消息後發現只有本身收到消息,其餘瀏覽器端收不到,如何解決這個問題,讓全部客戶端都能一塊兒聊天呢?
Channels引入了一個layer的概念,channel layer是一種通訊系統,容許多個consumer實例之間互相通訊,以及與外部Djanbo程序實現互通。
channel layer主要實現了兩種概念抽象:
channel name: channel實際上就是一個發送消息的通道,每一個Channel都有一個名稱,每個擁有這個名稱的人均可以往Channel裏邊發送消息
group: 多個channel能夠組成一個Group,每一個Group都有一個名稱,每個擁有這個名稱的人均可以往Group裏添加/刪除Channel,也能夠往Group裏發送消息,Group內的全部channel均可以收到,可是沒法發送給Group內的具體某個Channel
瞭解了上邊的概念,接下來咱們利用channel layer實現真正的聊天室,可以讓多個客戶端發送的消息被彼此看到
pip install channels_redis==2.3.3
複製代碼
CHANNEL_LAYERS = { 'default': { 'BACKEND': 'channels_redis.core.RedisChannelLayer', 'CONFIG': { "hosts": [('ops-coffee.cn', 6379)], }, }, } 複製代碼
添加channel以後咱們能夠經過如下命令檢查通道層是否可以正常工做
>python manage.py shell Python 3.6.3 (v3.6.3:2c5fed8, Oct 3 2017, 18:11:49) [MSC v.1900 64 bit (AMD64)] on win32 Type "help", "copyright", "credits" or "license" for more information. (InteractiveConsole) >>> import channels.layers >>> channel_layer = channels.layers.get_channel_layer() >>> >>> from asgiref.sync import async_to_sync >>> async_to_sync(channel_layer.send)('test_channel',{'site':'https://ops-coffee.cn'}) >>> async_to_sync(channel_layer.receive)('test_channel') {'site': 'https://ops-coffee.cn'} >>> 複製代碼
from asgiref.sync import async_to_sync from channels.generic.websocket import WebsocketConsumer import json class ChatConsumer(WebsocketConsumer): def connect(self): self.room_group_name = 'ops_coffee' # Join room group async_to_sync(self.channel_layer.group_add)( self.room_group_name, self.channel_name ) self.accept() def disconnect(self, close_code): # Leave room group async_to_sync(self.channel_layer.group_discard)( self.room_group_name, self.channel_name ) # Receive message from WebSocket def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json['message'] # Send message to room group async_to_sync(self.channel_layer.group_send)( self.room_group_name, { 'type': 'chat_message', 'message': message } ) # Receive message from room group def chat_message(self, event): message = '運維咖啡吧:' + event['message'] # Send message to WebSocket self.send(text_data=json.dumps({ 'message': message })) 複製代碼
這裏咱們設置了一個固定的房間名做爲Group name,全部的消息都會發送到這個Group裏邊,固然你也能夠經過參數的方式將房間名傳進來做爲Group name,從而創建多個Group,這樣能夠實現僅同房間內的消息互通
當咱們啓用了channel layer以後,全部與consumer之間的通訊將會變成異步的,因此必須使用async_to_sync
一個連接(channel)建立時,經過group_add
將channel添加到Group中,連接關閉經過group_discard
將channel從Group中剔除,收到消息時能夠調用group_send
方法將消息發送到Group,這個Group內全部的channel均可以收的到
group_send
中的type指定了消息處理的函數,這裏會將消息轉給chat_message
函數去處理
咱們前邊實現的consumer是同步的,爲了能有更好的性能,官方支持異步的寫法,只須要修改consumer.py便可
from channels.generic.websocket import AsyncWebsocketConsumer import json class ChatConsumer(AsyncWebsocketConsumer): async def connect(self): self.room_group_name = 'ops_coffee' # Join room group await self.channel_layer.group_add( self.room_group_name, self.channel_name ) await self.accept() async def disconnect(self, close_code): # Leave room group await self.channel_layer.group_discard( self.room_group_name, self.channel_name ) # Receive message from WebSocket async def receive(self, text_data): text_data_json = json.loads(text_data) message = text_data_json['message'] # Send message to room group await self.channel_layer.group_send( self.room_group_name, { 'type': 'chat_message', 'message': message } ) # Receive message from room group async def chat_message(self, event): message = '運維咖啡吧:' + event['message'] # Send message to WebSocket await self.send(text_data=json.dumps({ 'message': message })) 複製代碼
其實異步的代碼跟以前的差異不大,只有幾個小區別:
ChatConsumer由WebsocketConsumer
修改成了AsyncWebsocketConsumer
全部的方法都修改成了異步defasync def
用await
來實現異步I/O的調用
channel layer也再也不須要使用async_to_sync
了
好了,如今一個徹底異步且功能完整的聊天室已經構建完成了
我已經將以上的演示代碼上傳至Github方便你在實現的過程當中查看參考,具體地址爲:
相關文章推薦閱讀: