WebSocket - 開啟通往新世界的大門
WebSocket是什么?
WebSocket是一種在單個TCP連接上進行全雙工通訊的協議。WebSocket允許服務端主動向客戶端推送數據。在WebSocket協議中,客戶端瀏覽器和服務器只需要完成一次握手就可以創建持久性的連接,並在瀏覽器和服務器之間進行雙向的數據傳輸。
WebSocket有什么用?
WebSocket區別於HTTP協議的一個最為顯著的特點是,WebSocket協議可以由服務端主動發起消息,對於瀏覽器需要及時接收數據變化的場景非常適合,例如在Django中遇到一些耗時較長的任務我們通常會使用Celery來異步執行,那么瀏覽器如果想要獲取這個任務的執行狀態,在HTTP協議中只能通過輪訓的方式由瀏覽器不斷的發送請求給服務器來獲取最新狀態,這樣發送很多無用的請求不僅浪費資源,還不夠優雅,如果使用WebSokcet來實現就很完美了
WebSocket的另外一個應用場景就是下文要說的聊天室,一個用戶(瀏覽器)發送的消息需要實時的讓其他用戶(瀏覽器)接收,這在HTTP協議下是很難實現的,但WebSocket基於長連接加上可以主動給瀏覽器發消息的特性處理起來就游刃有余了
初步了解WebSocket之后,我們看看如何在Django中實現WebSocket
Channels
Django本身不支持WebSocket,但可以通過集成Channels框架來實現WebSocket
Channels是針對Django項目的一個增強框架,可以使Django不僅支持HTTP協議,還能支持WebSocket,MQTT等多種協議,同時Channels還整合了Django的auth以及session系統方便進行用戶管理及認證。
我下文所有的代碼實現使用以下python和Django版本
- python==3.6.3
- django==2.2
集成Channels
我假設你已經新建了一個django項目,項目名字就叫webapp,目錄結構如下
project
- webapp
- __init__.py
- settings.py
- urls.py
- wsgi.py
- manage.py
- 安裝channels
pip install channels==2.1.7
- 修改settings.py文件,
# 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
- setting.py的同級目錄下創建routing.py路由文件,routing.py類似於Django中的url.py指明websocket協議的路由
from channels.routing import ProtocolTypeRouter
application = ProtocolTypeRouter({
# 暫時為空,下文填充
})
- 運行Django項目
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協議實現消息的發送接收功能
- 先從路由入手,上邊我們已經創建了routing.py路由文件,現在來填充里邊的內容
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處理
- 接着編寫consumer,consumer類似django中的view,內容如下
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類會將所有收到的消息加上“運維咖啡吧:”的前綴發送給客戶端
- 最后我們在html模板頁面添加websocket支持
{% 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消息
- 完成前邊的代碼,一個可以聊天的websocket頁面就完成了,運行項目,在瀏覽器中輸入消息就會通過websocket-->rouging.py-->consumer.py處理后返回給前端

啟用Channel Layer
上邊的例子我們已經實現了消息的發送和接收,但既然是聊天室,肯定要支持多人同時聊天的,當我們打開多個瀏覽器分別輸入消息后發現只有自己收到消息,其他瀏覽器端收不到,如何解決這個問題,讓所有客戶端都能一起聊天呢?
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實現真正的聊天室,能夠讓多個客戶端發送的消息被彼此看到
- 官方推薦使用redis作為channel layer,所以先安裝channels_redis
pip install channels_redis==2.3.3
- 然后修改settings.py添加對layer的支持
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'}
>>>
- consumer做如下修改引入channel layer
文章未完,全部內容請關注公眾號【運維咖啡吧】或個人網站https://ops-coffee.cn查看,運維咖啡吧專注於原創精品內容分享,感謝您的支持

