在這個例子中,我們將使用Django Channels來創建一個實時在線應用,當用戶登錄或下線時,這個應用可以自動更新在線的用戶列表
使用WebSockets(通過Django Channels實現)可以管理客戶端和服務器端之間的通信,只要用戶登錄,這個事件將會廣播至每個連接的用戶,他們的瀏覽器會自動刷新頁面。
運行環境:
- Python(v3.6.0)
- Django(v1.10.5)
- Django Channels(v1.0.3)
- Redis(v3.2.8)
目標:
- 通過Django Channels使Django項目支持Web sockets
- 在Django和Redis服務器之間建立連接
- 使用Django中的basic user authentication
- 用戶登錄或登出時發出Django信號
首先創建一個使用Pyenv創建一個虛擬環境以及安裝第三方模塊
$ pip install django==1.10.5 channels==1.0.2 asgi_redis==1.0.0
$ django-admin.py startproject example_channels
$ cd example_channels
$ python manage.py startapp example
$ python manage.py migrate
下載和安裝Docker(Mac)
在Docker中啟動Redis服務docker run -p 6379:6379 -d registry.alauda.cn/library/redis:2.8
setting.py:
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',
}
}
WebSockets 101
正常情況下,Django使用HTTP請求實現客戶端和服務器端的通信:
- 客戶端發送HTTP請求到服務器端
- Django解析請求,提取URL,并將其和view進行匹配
- view處理請求并返回HTTP Response至客戶端
不同于HTTP請求,WebSockets協議使用雙向直接通信,也就是說不需要客戶端發送請求,服務器端就可以向發送數據。HTTP協議中,只有客戶端可以發送請求和接收響應,WebSockets協議中,服務器端可以同時與多個客戶端進行通信。我們將使用ws://
前綴而不是http://
- view處理請求并返回HTTP Response至客戶端
Consumers and Groups
創建第一個consumer,它可以處理客戶端和服務端的基本連接。
example_channels/example/consumers.py:
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中的view,任何用戶連接到我們應用都會被加入到'users'組,并且接收服務器端發送的消息。當客戶端與我們的應用斷開連接,這個連接通道將會'user'組中移除,并且停止接收服務器端的消息。
下一步建立路由routes,它的作用和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),
]
注意到,我們現在將consumer方法和WebSockets相關聯。
Templates
example_channels/example/templates/example/_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>
example_channels/example/templates/example/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與服務器創建連接。
Views
創建一個視圖類來渲染和返回user_list.html:
from django.shortcuts import render
def user_list(request):
return render(request, 'example/user_list.html')
為user_list視圖類配置路由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:
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')),
]
Test
啟動項目,觀察控制臺shell輸出
[2017/02/19 23:24:57] HTTP GET / 200 [0.02, 127.0.0.1:52757]
[2017/02/19 23:24:58] WebSocket HANDSHAKING /users/ [127.0.0.1:52789]
[2017/02/19 23:25:03] WebSocket DISCONNECT /users/ [127.0.0.1:52789]
User Authentication
現在,我們已經可以通過WebSocket建立一個連接,下一步將處理用戶認證模塊(User Authentication)。記住:我們期望一個用戶可以登錄應用并且可以看到其他已經注冊的用戶。第一步,創建一個簡單的用戶登錄界面:
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帶有支持通用認證功能的表單,我們可以使用AuthenticationForm
來處理用戶登錄。此表單檢查提供的用戶名和密碼,然后在找到經過驗證的用戶時返回一個用戶對象。 我們登錄驗證的用戶并將其重定向到我們的主頁。 用戶還必須能夠注銷應用程序,因此我們創建了一個注銷視圖,該視圖提供了該功能,然后將用戶重定向至登錄頁面。
更新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')
]
創建一個注冊的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})
為sign_up配置URL:
url(r'^sign_up/$', sign_up, name='sign_up'),
Login Alerts
我們有基本的用戶認證功能,但我們仍然需要顯示用戶列表,并且我們需要服務器在用戶登錄和注銷時告訴用戶組。 重寫consumer函數,使得在客戶端連接之后和在客戶端斷開連接之前立即發送消息。 消息數據將包含用戶的用戶名和連接狀態。
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)
我們在函數中添加了裝飾器以從Django會話中獲取用戶。 而且,所有消息都必須是JSON序列化的,所以我們將數據轉儲到JSON字符串中。
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不會記錄用戶是否登錄,所以需要創建一個簡單的模型來做這件事。 在example_channels / example / models.py中創建一個LoggedInUser
模型,該模型與User模型是一對一的關系。
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
實例,反之用于注銷時會刪除一個LoggedInUser
實例。
數據庫遷移:
$ python manage.py makemigrations
$ python manage.py migrate
接下來,在example_channels / example / views.py中更新我們的user_list視圖,以獲取要呈現的用戶列表:
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相關聯,那么我們將用戶的狀態記錄為“Online”,如果不是,則該用戶是“Offline”。 我們還在我們的用戶列表和注銷視圖中添加了@login_required裝飾器,以便僅限注冊用戶訪問。
此時,用戶可以登錄和注銷,這將觸發服務器向客戶端發送消息,但我們無法知道用戶首次登錄時哪些用戶登錄。用戶僅在其他用戶 狀態改變。 這就是LoggedInUser發揮作用的地方,但我們需要一種方式在用戶登錄時創建LoggedInUser實例,然后在用戶注銷時將其刪除。
Django庫有信號量的功能,當發生某些操作時它會廣播通知。 應用程序可以偵聽這些通知,然后對其執行操作。 我們可以利用兩個有用的內置信號(user_logged_in和user_logged_out)來處理我們的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'
------------------------------------------------EOF----------------------------------------------------