原文:https://realpython.com/getting-started-with-django-channels/
本文中,我們將使用 Django Channels來構建一個實時應用程序:當客戶端上線或下線時,實時更新用戶列表數據。使用 WebSockets (通過 Django Channels) 技術進行客戶端和服務器之間的通信,當有客戶端上線,服務器會向所有連接的客戶端發送一個廣播,并自動更新客戶端屏幕顯示而不用刷新頁面。
理解本文需要的知識儲備:
Django 開發經驗
WebSocket 概念
項目任務:
為 Django 項目添加 WebSocket 的支持(通過 Django Channels)
Django 使用 Redis,建立簡單的連接
實現基本的用戶身份驗證
使用 Django 信號(Django Signals)機制來操作用戶上下線的動作
將要用到的工具包:
Python (v3.6.0)
Django (v1.10.5)
Django Channels (v1.0.3)
Redis (v3.2.8)
開始
首先創建一個新的虛擬環境來隔離我們項目的依賴包的安裝
$ mkdir django-example-channels
$ cd django-example-channels
$ python3.6 -m venv env
$ source env/bin/activate
(env)$
安裝 Django, Django Channels, and ASGI Redis,創建一個新的 Django 項目和 app
(ENV)$ PIP安裝django的== 1個 .10.5 通道== 1 .0.2 asgi_redis == 1 .0.0
(ENV)$ django-admin.py startproject命令example_channels
(ENV)$ CD example_channels
(ENV)$蟒manage.py的startApp example
(env)$ python manage.py migrate
下載安裝 Redis
啟動 Redis 服務默認使用 6379 端口,Django 將使用該端口連接 Redis 服務。
更新項目配置文件 settings.py 中的 INSTALLED_APPS 項
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'channels',
'example',
]
配置 CHANNEL_LAYERS:
CHANNEL_LAYERS = {
'default': {
'BACKEND': 'asgi_redis.RedisChannelLayer',
'CONFIG': {
'hosts': [('localhost', 6379)],
},
'ROUTING': 'example_channels.routing.channel_routing',
}
}
WebSocket 101
通常 Django 使用HTTP在客戶機和服務器之間通信:
客戶端發送 HTTP 請求
Django 解析請求,提取 URL 然后將它匹配到一個視圖函數進行處理
視圖處理請求并返回 HTTP 響應
與HTTP
不同的是WebSocket
協議允許雙向通信,這意味著服務器可以將數據推送到客戶端,而無需用戶請求。HTTP中只有客戶端請求然后得到響應,而WebSocket
協議中,服務器可以同時與多個客戶端進行通信,下面我們將要演示的使用ws://
前綴,而不是http://
有什么不清楚的請自行查閱相關CHANNEL文檔
Consumers and Groups
新建一個文件 example_channels/example/consumers.py
,創建首個 consumer,它負責處理客戶端和服務器的基礎連接。
from channels import Group
def ws_connect(message):
Group('users').add(message.reply_channel)
def ws_disconnect(message):
Group('users').discard(message.reply_channel)
Consumer 對應到Django的視圖,任何連接到服務器的客戶端用戶將被添加到“users”群組,可以接收到服務器發送的信息。當客戶端離線時,該用戶通道(channel)將會被移除出群組中,用戶無法接收到信息。
接下來,進行路由的設置,它的工作方式與Django URL配置幾乎相同,將以下代碼添加到 example_channels/routing.py 這個新文件中:
from channels.routing import route
from example.consumers import ws_connect, ws_disconnect
channel_routing = [
route('websocket.connect', ws_connect),
route('websocket.disconnect', ws_disconnect),
]
上面我們通過定義一個 channel_routing
替換 urlpatterns
,用 route()
替換掉 url()
。將我們的 consumer 處理函數匹配到 WebSockets。
模板
編寫可以進行 WebSockets的Html 代碼,構建項目模板文件夾 example_channels/example/templates/example
,新建:
a _base.html
:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<link rel="stylesheet">
<title>Example Channels</title>
</head>
<body>
<div class="container">
<br>
{% block content %}{% endblock content %}
</div>
<script src="http://code.jquery.com/jquery-3.1.1.min.js"></script>
{% block script %}{% endblock script %}
</body>
</html>
user_list.html
:
{% extends 'example/_base.html' %}
{% block content %}{% endblock content %}
{% block script %}
<script>
var socket = new WebSocket('ws://' + window.location.host + '/users/');
socket.onopen = function open() {
console.log('WebSockets connection created.');
};
if (socket.readyState == WebSocket.OPEN) {
socket.onopen();
}
</script>
{% endblock script %}
現在,當我們的客戶端成功使用 WebSocket 建立和服務器的連接時,我們可以在后臺的命令行看到相應信息。
視圖
在example_channels/example/views.py
文件中,創建支持Django視圖的模板渲染的代碼:
from django.shortcuts import render
def user_list(request):
return render(request, 'example/user_list.html')
將URL添加到 example_channels/example/urls.py
中:
from django.conf.urls import url
from example.views import user_list
urlpatterns = [
url(r'^$', user_list, name='user_list'),
]
將 example_channels/example_channels/urls.py中的地址,更新到項目的 URL 中:
from django.conf.urls import include, url
from django.contrib import admin
urlpatterns = [
url(r'^admin/', admin.site.urls),
url(r'^', include('example.urls', namespace='example')),
]
測試
啟動項目進行測試:
(env)$ python manage.py runserver
您也可以在兩個不同的終端上運行 pythonManage.py runserver-noWorker
和 pythonManage.py runWorker
,以作為兩個獨立的進程測試接口服務器和工作服務器。兩種方法都有效!
現在你訪問 http://localhost:8000/ 在后臺的命令行終端應該可以看到類似以下的信息:
[2018/02/19 23:24:57] HTTP GET / 200 [0.02, 127.0.0.1:52757]
[2018/02/19 23:24:58] WebSocket HANDSHAKING /users/ [127.0.0.1:52789]
[2018/02/19 23:25:03] WebSocket DISCONNECT /users/ [127.0.0.1:52789]
用戶身份驗證
接下來我們需要做的就是處理用戶的身份驗證,我們目標是用戶登錄到系統后,能夠看到本組中其他成員的列表。首先構建用戶創建賬號和登錄的方式,新建一個簡單的登錄頁面,用戶可以通過賬號和密碼進行登錄。
example_channels/example/templates/example/log_in.html:
{% extends 'example/_base.html' %}
{% block content %}
<form action="{% url 'example:log_in' %}" method="post">
{% csrf_token %}
{% for field in form %}
<div>
{{ field.label_tag }}
{{ field }}
</div>
{% endfor %}
<button type="submit">Log in</button>
</form>
<p>Don't have an account? <a href="{% url 'example:sign_up' %}">Sign up!</a></p>
{% endblock content %}
接下來更新 example_channels/example/views.py:
from django.contrib.auth import login, logout
from django.contrib.auth.forms import AuthenticationForm
from django.core.urlresolvers import reverse
from django.shortcuts import render, redirect
def user_list(request):
return render(request, 'example/user_list.html')
def log_in(request):
form = AuthenticationForm()
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
login(request, form.get_user())
return redirect(reverse('example:user_list'))
else:
print(form.errors)
return render(request, 'example/log_in.html', {'form': form})
def log_out(request):
logout(request)
return redirect(reverse('example:log_in'))
Django 本身自帶通用身份驗證表單功能,我們可以用它來提供用戶的登錄驗證。表單檢驗用戶的賬號和密碼是否匹配,驗證通過后返回一個 User對象。用戶登錄后將重定向到項目的主頁。用戶也應該可以進行注銷的操作,所以我們繼續創建一個注銷視圖,用戶注銷后將轉回登錄頁面。
更新 example_channels/example/urls.py:
from django.conf.urls import url
from example.views import log_in, log_out, user_list
urlpatterns = [
url(r'^log_in/$', log_in, name='log_in'),
url(r'^log_out/$', log_out, name='log_out'),
url(r'^$', user_list, name='user_list')
]
我們還需要一個注冊頁面來提供新用戶注冊,example_channels/example/templates/example/sign_up.html
:
{% extends 'example/_base.html' %}
{% block content %}
<form action="{% url 'example:sign_up' %}" method="post">
{% csrf_token %}
{% for field in form %}
<div>
{{ field.label_tag }}
{{ field }}
</div>
{% endfor %}
<button type="submit">Sign up</button>
<p>Already have an account? <a href="{% url 'example:log_in' %}">Log in!</a></p>
</form>
{% endblock content %}
登錄和注冊頁面類似并相互鏈接。然后在視圖中加入函數:
def sign_up(request):
form = UserCreationForm()
if request.method == 'POST':
form = UserCreationForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('example:log_in'))
else:
print(form.errors)
return render(request, 'example/sign_up.html', {'form': form})
同樣我們使用自帶的表單來提供用戶注冊處理,注冊成功后將定向到登錄頁面。要記得在代碼中導入表單模塊:
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
再次更新 example_channels/example/urls.py:
from django.conf.urls import url
from example.views import log_in, log_out, sign_up, user_list
urlpatterns = [
url(r'^log_in/$', log_in, name='log_in'),
url(r'^log_out/$', log_out, name='log_out'),
url(r'^sign_up/$', sign_up, name='sign_up'),
url(r'^$', user_list, name='user_list')
]
到此,我們重新打開瀏覽器訪問 http://localhost:8000/sign_up/ ,填好注冊信息就創建我們第一個注冊用戶。(默認用戶是michael,密碼 johnson123)Sign_up
視圖將我們重定向到log_in
視圖,我們可以對新創建的用戶進行身份驗證。登錄后,我們可以測試新的身份驗證視圖。然后使用“注冊”表單創建幾個新用戶,為下一節做準備。
登錄提醒
我們已經構建了基本的登錄驗證功能,但還沒有完成用戶列表的顯示,還要實現當用戶登錄下線時服務器自動更新這個列表。接下來,我們將更新消費者函數,以便當用戶登錄或退出時發送通知消息。該消息包括用戶名和連接狀態信息。
example_channels/example/consumers.py:
import json
from channels import Group
from channels.auth import channel_session_user, channel_session_user_from_http
@channel_session_user_from_http
def ws_connect(message):
Group('users').add(message.reply_channel)
Group('users').send({
'text': json.dumps({
'username': message.user.username,
'is_logged_in': True
})
})
@channel_session_user
def ws_disconnect(message):
Group('users').send({
'text': json.dumps({
'username': message.user.username,
'is_logged_in': False
})
})
Group('users').discard(message.reply_channel)
example_channels/example/templates/example/user_list.html:
{% extends 'example/_base.html' %}
{% block content %}
<a href="{% url 'example:log_out' %}">Log out</a>
<br>
<ul>
{% for user in users %}
<!-- NOTE: We escape HTML to prevent XSS attacks. -->
<li data-username="{{ user.username|escape }}">
{{ user.username|escape }}: {{ user.status|default:'Offline' }}
</li>
{% endfor %}
</ul>
{% endblock content %}
{% block script %}
<script>
var socket = new WebSocket('ws://' + window.location.host + '/users/');
socket.onopen = function open() {
console.log('WebSockets connection created.');
};
socket.onmessage = function message(event) {
var data = JSON.parse(event.data);
// NOTE: We escape JavaScript to prevent XSS attacks.
var username = encodeURI(data['username']);
var user = $('li').filter(function () {
return $(this).data('username') == username;
});
if (data['is_logged_in']) {
user.html(username + ': Online');
}
else {
user.html(username + ': Offline');
}
};
if (socket.readyState == WebSocket.OPEN) {
socket.onopen();
}
</script>
{% endblock script %}
在主頁上,我們擴展用戶列表用來顯示用戶數據,將每個用戶名存儲為一個數據屬性,方便在DOM
中搜索到該數據項。向WebSocket
添加一個事件監聽器,用來處理服務器的消息。當收到消息時,解析JSON
數據,定位到該用戶的<li>
元素,更新該用戶狀態。
Django不會跟蹤用戶是否登錄,因此我們還要創建一個簡單的模型來實現這個功能。創建一個LoggedInUser
模型,與用戶模型進行一對一的連接。
example_channels/example/models.py:
from django.conf import settings
from django.db import models
class LoggedInUser(models.Model):
user = models.OneToOneField(
settings.AUTH_USER_MODEL, related_name='logged_in_user')
當用戶登錄時應用將創建一個 LoggedInUser 實例,當用戶退出時這個實例將被刪除。更新我們的數據庫:
(env)$ python manage.py makemigrations
(env)$ python manage.py migrate
更新用戶列表視圖函數,提供檢索需要渲染的用戶列表:
example_channels/example/views.py
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
from django.contrib.auth.forms import AuthenticationForm, UserCreationForm
from django.core.urlresolvers import reverse
from django.shortcuts import render, redirect
User = get_user_model()
@login_required(login_url='/log_in/')
def user_list(request):
"""
NOTE: This is fine for demonstration purposes, but this should be
refactored before we deploy this app to production.
Imagine how 100,000 users logging in and out of our app would affect
the performance of this code!
"""
users = User.objects.select_related('logged_in_user')
for user in users:
user.status = 'Online' if hasattr(user, 'logged_in_user') else 'Offline'
return render(request, 'example/user_list.html', {'users': users})
def log_in(request):
form = AuthenticationForm()
if request.method == 'POST':
form = AuthenticationForm(data=request.POST)
if form.is_valid():
login(request, form.get_user())
return redirect(reverse('example:user_list'))
else:
print(form.errors)
return render(request, 'example/log_in.html', {'form': form})
@login_required(login_url='/log_in/')
def log_out(request):
logout(request)
return redirect(reverse('example:log_in'))
def sign_up(request):
form = UserCreationForm()
if request.method == 'POST':
form = UserCreationForm(data=request.POST)
if form.is_valid():
form.save()
return redirect(reverse('example:log_in'))
else:
print(form.errors)
return render(request, 'example/sign_up.html', {'form': form})
如果該用戶有對應的 LoggedInUser
則標記為在線,否則標記為離線。添加了一個@login_required
裝飾器,用來限制僅僅對注冊用戶的訪問。
添加以下導入包
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
現在,用戶可以登錄,注銷,這些會觸發服務器向客戶端發送消息。
但是當用戶第一次登錄時,我們無法知道哪些用戶已經登錄。用戶只在其他用戶的狀態更改時才看到更新。這就是LoggedInUser
發揮作用的地方,但我們需要一種方法,在用戶登錄時創建LoggedInUser
實例,然后在該用戶注銷時將其刪除。
但我們現在還沒有辦法知道當用戶第一個登錄時是哪一位。只用當其他用戶登錄狀態更新是我們才能看到。
Django庫包含一個稱為Signals
的特性,當發生某些操作時,它會廣播通知。應用程序可以監聽這些通知,然后對它們采取行動。我們可以利用兩個有用的內置信號(user_login和user_logout)來處理LoggedInUser
行為。
example_channels/example/signals.py****:
from django.contrib.auth import user_logged_in, user_logged_out
from django.dispatch import receiver
from example.models import LoggedInUser
@receiver(user_logged_in)
def on_user_login(sender, **kwargs):
LoggedInUser.objects.get_or_create(user=kwargs.get('user'))
@receiver(user_logged_out)
def on_user_logout(sender, **kwargs):
LoggedInUser.objects.filter(user=kwargs.get('user')).delete()
example_channels/example/apps.py
from django.apps import AppConfig
class ExampleConfig(AppConfig):
name = 'example'
def ready(self):
import example.signals
example_channels/example/init.py
default_app_config = 'example.apps.ExampleConfig'
完整性檢查
到此,代碼部分已經完成,我們用多個用戶賬號連接到服務器來測試一下我們的應用。啟動 Django 服務,登錄系統,訪問項目主頁。我們應該能夠看到所有的用戶列表,此時用戶的狀態都是“離線”。打開新的瀏覽器匿名窗口,用另一個賬號登錄,這時各個窗口的用戶列表會自動更新到“在線”狀態。你可以通過不同的瀏覽器、設備來測試登錄登出。
查看客戶端瀏覽器上的開發人員控制臺和終端中的服務器活動,你可以觀察到:當用戶登錄時,WebSocket
連接被創建,當用戶注銷時,WebSocket
連接被銷毀。
總結
本文我們討論了:
Django Channels
WebSockets
用戶身份驗證
Django 信號
部分前端開發技術
重要的是 Django Channels
擴展了 Django 框架的傳統功能,通過 WebSockets
我們可以將消息從服務器直接發送到客戶端。這個功能可以讓我們進一步做出很多有意思的東西,比如聊天室、多人在線游戲、能夠實時通信的協作應用。一般的應用使用 WebSockets
,在服務器完成任務后向客戶端發送狀態更新來代替傳統的定期輪詢服務器,從而得到性能改進。
本文只是簡單介紹了 Django Channels 的基本使用,感興趣的童鞋可以閱讀 Django Channels 項目的文檔,看看你還可以用它來實現什么有趣的東東。