原文:https://realpython.com/getting-started-with-django-channels/
本文中洼冻,我們將使用 Django Channels來(lái)構(gòu)建一個(gè)實(shí)時(shí)應(yīng)用程序:當(dāng)客戶端上線或下線時(shí)租漂,實(shí)時(shí)更新用戶列表數(shù)據(jù)搬卒。使用 WebSockets (通過(guò) Django Channels) 技術(shù)進(jìn)行客戶端和服務(wù)器之間的通信,當(dāng)有客戶端上線胡诗,服務(wù)器會(huì)向所有連接的客戶端發(fā)送一個(gè)廣播,并自動(dòng)更新客戶端屏幕顯示而不用刷新頁(yè)面。
理解本文需要的知識(shí)儲(chǔ)備:
Django 開發(fā)經(jīng)驗(yàn)
WebSocket 概念
項(xiàng)目任務(wù):
為 Django 項(xiàng)目添加 WebSocket 的支持(通過(guò) Django Channels)
Django 使用 Redis学辱,建立簡(jiǎn)單的連接
實(shí)現(xiàn)基本的用戶身份驗(yàn)證
使用 Django 信號(hào)(Django Signals)機(jī)制來(lái)操作用戶上下線的動(dòng)作
將要用到的工具包:
Python (v3.6.0)
Django (v1.10.5)
Django Channels (v1.0.3)
Redis (v3.2.8)
開始
首先創(chuàng)建一個(gè)新的虛擬環(huán)境來(lái)隔離我們項(xiàng)目的依賴包的安裝
$ mkdir django-example-channels
$ cd django-example-channels
$ python3.6 -m venv env
$ source env/bin/activate
(env)$
安裝 Django, Django Channels, and ASGI Redis,創(chuàng)建一個(gè)新的 Django 項(xiàng)目和 app
(ENV)$ PIP安裝django的== 1個(gè) .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
啟動(dòng) Redis 服務(wù)默認(rèn)使用 6379 端口环形,Django 將使用該端口連接 Redis 服務(wù)策泣。
更新項(xiàng)目配置文件 settings.py 中的 INSTALLED_APPS 項(xiàng)
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在客戶機(jī)和服務(wù)器之間通信:
客戶端發(fā)送 HTTP 請(qǐng)求
Django 解析請(qǐng)求,提取 URL 然后將它匹配到一個(gè)視圖函數(shù)進(jìn)行處理
視圖處理請(qǐng)求并返回 HTTP 響應(yīng)
與HTTP
不同的是WebSocket
協(xié)議允許雙向通信抬吟,這意味著服務(wù)器可以將數(shù)據(jù)推送到客戶端萨咕,而無(wú)需用戶請(qǐng)求。HTTP中只有客戶端請(qǐng)求然后得到響應(yīng)火本,而WebSocket
協(xié)議中危队,服務(wù)器可以同時(shí)與多個(gè)客戶端進(jìn)行通信,下面我們將要演示的使用ws://
前綴钙畔,而不是http://
有什么不清楚的請(qǐng)自行查閱相關(guān)CHANNEL文檔
Consumers and Groups
新建一個(gè)文件 example_channels/example/consumers.py
茫陆,創(chuàng)建首個(gè) consumer,它負(fù)責(zé)處理客戶端和服務(wù)器的基礎(chǔ)連接擎析。
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 對(duì)應(yīng)到Django的視圖簿盅,任何連接到服務(wù)器的客戶端用戶將被添加到“users”群組,可以接收到服務(wù)器發(fā)送的信息。當(dāng)客戶端離線時(shí)桨醋,該用戶通道(channel)將會(huì)被移除出群組中棚瘟,用戶無(wú)法接收到信息。
接下來(lái)喜最,進(jìn)行路由的設(shè)置偎蘸,它的工作方式與Django URL配置幾乎相同,將以下代碼添加到 example_channels/routing.py 這個(gè)新文件中:
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),
]
上面我們通過(guò)定義一個(gè) channel_routing
替換 urlpatterns
瞬内,用 route()
替換掉 url()
迷雪。將我們的 consumer 處理函數(shù)匹配到 WebSockets。
模板
編寫可以進(jìn)行 WebSockets的Html 代碼虫蝶,構(gòu)建項(xiàng)目模板文件夾 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 %}
現(xiàn)在,當(dāng)我們的客戶端成功使用 WebSocket 建立和服務(wù)器的連接時(shí)秉扑,我們可以在后臺(tái)的命令行看到相應(yīng)信息慧邮。
視圖
在example_channels/example/views.py
文件中,創(chuàng)建支持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中的地址舟陆,更新到項(xiàng)目的 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')),
]
測(cè)試
啟動(dòng)項(xiàng)目進(jìn)行測(cè)試:
(env)$ python manage.py runserver
您也可以在兩個(gè)不同的終端上運(yùn)行 pythonManage.py runserver-noWorker
和 pythonManage.py runWorker
误澳,以作為兩個(gè)獨(dú)立的進(jìn)程測(cè)試接口服務(wù)器和工作服務(wù)器。兩種方法都有效秦躯!
現(xiàn)在你訪問(wèn) http://localhost:8000/ 在后臺(tái)的命令行終端應(yīng)該可以看到類似以下的信息:
[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]
用戶身份驗(yàn)證
接下來(lái)我們需要做的就是處理用戶的身份驗(yàn)證忆谓,我們目標(biāo)是用戶登錄到系統(tǒng)后,能夠看到本組中其他成員的列表踱承。首先構(gòu)建用戶創(chuàng)建賬號(hào)和登錄的方式倡缠,新建一個(gè)簡(jiǎn)單的登錄頁(yè)面,用戶可以通過(guò)賬號(hào)和密碼進(jìn)行登錄茎活。
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 %}
接下來(lái)更新 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 本身自帶通用身份驗(yàn)證表單功能昙沦,我們可以用它來(lái)提供用戶的登錄驗(yàn)證。表單檢驗(yàn)用戶的賬號(hào)和密碼是否匹配载荔,驗(yàn)證通過(guò)后返回一個(gè) User對(duì)象盾饮。用戶登錄后將重定向到項(xiàng)目的主頁(yè)。用戶也應(yīng)該可以進(jìn)行注銷的操作懒熙,所以我們繼續(xù)創(chuàng)建一個(gè)注銷視圖丘损,用戶注銷后將轉(zhuǎn)回登錄頁(yè)面。
更新 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')
]
我們還需要一個(gè)注冊(cè)頁(yè)面來(lái)提供新用戶注冊(cè)工扎,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 %}
登錄和注冊(cè)頁(yè)面類似并相互鏈接徘钥。然后在視圖中加入函數(shù):
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})
同樣我們使用自帶的表單來(lái)提供用戶注冊(cè)處理,注冊(cè)成功后將定向到登錄頁(yè)面肢娘。要記得在代碼中導(dǎo)入表單模塊:
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')
]
到此呈础,我們重新打開瀏覽器訪問(wèn) http://localhost:8000/sign_up/ 舆驶,填好注冊(cè)信息就創(chuàng)建我們第一個(gè)注冊(cè)用戶。(默認(rèn)用戶是michael猪落,密碼 johnson123)Sign_up
視圖將我們重定向到log_in
視圖贞远,我們可以對(duì)新創(chuàng)建的用戶進(jìn)行身份驗(yàn)證畴博。登錄后笨忌,我們可以測(cè)試新的身份驗(yàn)證視圖。然后使用“注冊(cè)”表單創(chuàng)建幾個(gè)新用戶俱病,為下一節(jié)做準(zhǔn)備官疲。
登錄提醒
我們已經(jīng)構(gòu)建了基本的登錄驗(yàn)證功能,但還沒(méi)有完成用戶列表的顯示亮隙,還要實(shí)現(xiàn)當(dāng)用戶登錄下線時(shí)服務(wù)器自動(dòng)更新這個(gè)列表途凫。接下來(lái),我們將更新消費(fèi)者函數(shù)溢吻,以便當(dāng)用戶登錄或退出時(shí)發(fā)送通知消息维费。該消息包括用戶名和連接狀態(tài)信息。
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 %}
在主頁(yè)上促王,我們擴(kuò)展用戶列表用來(lái)顯示用戶數(shù)據(jù)犀盟,將每個(gè)用戶名存儲(chǔ)為一個(gè)數(shù)據(jù)屬性,方便在DOM
中搜索到該數(shù)據(jù)項(xiàng)蝇狼。向WebSocket
添加一個(gè)事件監(jiān)聽器阅畴,用來(lái)處理服務(wù)器的消息。當(dāng)收到消息時(shí)迅耘,解析JSON
數(shù)據(jù)贱枣,定位到該用戶的<li>
元素,更新該用戶狀態(tài)颤专。
Django不會(huì)跟蹤用戶是否登錄纽哥,因此我們還要?jiǎng)?chuàng)建一個(gè)簡(jiǎn)單的模型來(lái)實(shí)現(xiàn)這個(gè)功能。創(chuàng)建一個(gè)LoggedInUser
模型栖秕,與用戶模型進(jìn)行一對(duì)一的連接春塌。
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')
當(dāng)用戶登錄時(shí)應(yīng)用將創(chuàng)建一個(gè) LoggedInUser 實(shí)例,當(dāng)用戶退出時(shí)這個(gè)實(shí)例將被刪除累魔。更新我們的數(shù)據(jù)庫(kù):
(env)$ python manage.py makemigrations
(env)$ python manage.py migrate
更新用戶列表視圖函數(shù)摔笤,提供檢索需要渲染的用戶列表:
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})
如果該用戶有對(duì)應(yīng)的 LoggedInUser
則標(biāo)記為在線,否則標(biāo)記為離線垦写。添加了一個(gè)@login_required
裝飾器吕世,用來(lái)限制僅僅對(duì)注冊(cè)用戶的訪問(wèn)。
添加以下導(dǎo)入包
from django.contrib.auth import get_user_model, login, logout
from django.contrib.auth.decorators import login_required
現(xiàn)在梯投,用戶可以登錄命辖,注銷况毅,這些會(huì)觸發(fā)服務(wù)器向客戶端發(fā)送消息。
但是當(dāng)用戶第一次登錄時(shí)尔艇,我們無(wú)法知道哪些用戶已經(jīng)登錄尔许。用戶只在其他用戶的狀態(tài)更改時(shí)才看到更新。這就是LoggedInUser
發(fā)揮作用的地方终娃,但我們需要一種方法味廊,在用戶登錄時(shí)創(chuàng)建LoggedInUser
實(shí)例,然后在該用戶注銷時(shí)將其刪除棠耕。
但我們現(xiàn)在還沒(méi)有辦法知道當(dāng)用戶第一個(gè)登錄時(shí)是哪一位余佛。只用當(dāng)其他用戶登錄狀態(tài)更新是我們才能看到。
Django庫(kù)包含一個(gè)稱為Signals
的特性窍荧,當(dāng)發(fā)生某些操作時(shí)辉巡,它會(huì)廣播通知。應(yīng)用程序可以監(jiān)聽這些通知蕊退,然后對(duì)它們采取行動(dòng)郊楣。我們可以利用兩個(gè)有用的內(nèi)置信號(hào)(user_login和user_logout)來(lái)處理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'
完整性檢查
到此瓤荔,代碼部分已經(jīng)完成净蚤,我們用多個(gè)用戶賬號(hào)連接到服務(wù)器來(lái)測(cè)試一下我們的應(yīng)用。啟動(dòng) Django 服務(wù)茉贡,登錄系統(tǒng)塞栅,訪問(wèn)項(xiàng)目主頁(yè)。我們應(yīng)該能夠看到所有的用戶列表腔丧,此時(shí)用戶的狀態(tài)都是“離線”放椰。打開新的瀏覽器匿名窗口,用另一個(gè)賬號(hào)登錄愉粤,這時(shí)各個(gè)窗口的用戶列表會(huì)自動(dòng)更新到“在線”狀態(tài)砾医。你可以通過(guò)不同的瀏覽器、設(shè)備來(lái)測(cè)試登錄登出衣厘。
查看客戶端瀏覽器上的開發(fā)人員控制臺(tái)和終端中的服務(wù)器活動(dòng)如蚜,你可以觀察到:當(dāng)用戶登錄時(shí),WebSocket
連接被創(chuàng)建影暴,當(dāng)用戶注銷時(shí)错邦,WebSocket
連接被銷毀。
總結(jié)
本文我們討論了:
Django Channels
WebSockets
用戶身份驗(yàn)證
Django 信號(hào)
部分前端開發(fā)技術(shù)
重要的是 Django Channels
擴(kuò)展了 Django 框架的傳統(tǒng)功能型宙,通過(guò) WebSockets
我們可以將消息從服務(wù)器直接發(fā)送到客戶端撬呢。這個(gè)功能可以讓我們進(jìn)一步做出很多有意思的東西,比如聊天室妆兑、多人在線游戲魂拦、能夠?qū)崟r(shí)通信的協(xié)作應(yīng)用毛仪。一般的應(yīng)用使用 WebSockets
,在服務(wù)器完成任務(wù)后向客戶端發(fā)送狀態(tài)更新來(lái)代替?zhèn)鹘y(tǒng)的定期輪詢服務(wù)器芯勘,從而得到性能改進(jìn)箱靴。
本文只是簡(jiǎn)單介紹了 Django Channels 的基本使用,感興趣的童鞋可以閱讀 Django Channels 項(xiàng)目的文檔荷愕,看看你還可以用它來(lái)實(shí)現(xiàn)什么有趣的東東衡怀。