Django-channels實(shí)現(xiàn)websockets

在這個(gè)例子中,我們將使用Django Channels來創(chuàng)建一個(gè)實(shí)時(shí)在線應(yīng)用,當(dāng)用戶登錄或下線時(shí),這個(gè)應(yīng)用可以自動(dòng)更新在線的用戶列表

使用WebSockets(通過Django Channels實(shí)現(xiàn))可以管理客戶端和服務(wù)器端之間的通信,只要用戶登錄,這個(gè)事件將會(huì)廣播至每個(gè)連接的用戶,他們的瀏覽器會(huì)自動(dòng)刷新頁面。

運(yùn)行環(huán)境:

  • Python(v3.6.0)
  • Django(v1.10.5)
  • Django Channels(v1.0.3)
  • Redis(v3.2.8)

目標(biāo)

  • 通過Django Channels使Django項(xiàng)目支持Web sockets
  • 在Django和Redis服務(wù)器之間建立連接
  • 使用Django中的basic user authentication
  • 用戶登錄或登出時(shí)發(fā)出Django信號(hào)

首先創(chuàng)建一個(gè)使用Pyenv創(chuàng)建一個(gè)虛擬環(huán)境以及安裝第三方模塊

$ 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中啟動(dòng)Redis服務(wù)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設(shè)置默認(rèn)的后端和路由

CHANNEL_LAYERS = {
    'default': {
        'BACKEND': 'asgi_redis.RedisChannelLayer',
        'CONFIG': {
            'hosts': [('localhost', 6379)],
        },
        'ROUTING': 'example_channels.routing.channel_routing',
    }
}

WebSockets 101

正常情況下,Django使用HTTP請(qǐng)求實(shí)現(xiàn)客戶端和服務(wù)器端的通信:

    1. 客戶端發(fā)送HTTP請(qǐng)求到服務(wù)器端
    1. Django解析請(qǐng)求,提取URL,并將其和view進(jìn)行匹配
    1. view處理請(qǐng)求并返回HTTP Response至客戶端
      不同于HTTP請(qǐng)求,WebSockets協(xié)議使用雙向直接通信,也就是說不需要客戶端發(fā)送請(qǐng)求,服務(wù)器端就可以向發(fā)送數(shù)據(jù)。HTTP協(xié)議中,只有客戶端可以發(fā)送請(qǐng)求和接收響應(yīng),WebSockets協(xié)議中,服務(wù)器端可以同時(shí)與多個(gè)客戶端進(jìn)行通信。我們將使用ws://前綴而不是http://

Consumers and Groups

創(chuàng)建第一個(gè)consumer,它可以處理客戶端和服務(wù)端的基本連接。
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相當(dāng)于django中的view,任何用戶連接到我們應(yīng)用都會(huì)被加入到'users'組,并且接收服務(wù)器端發(fā)送的消息。當(dāng)客戶端與我們的應(yīng)用斷開連接,這個(gè)連接通道將會(huì)'user'組中移除,并且停止接收服務(wù)器端的消息。

下一步建立路由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),
]

注意到,我們現(xiàn)在將consumer方法和WebSockets相關(guān)聯(lián)。

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 %}

現(xiàn)在客戶端可以通過WebSocket與服務(wù)器創(chuàng)建連接。

Views

創(chuàng)建一個(gè)視圖類來渲染和返回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

啟動(dòng)項(xiàng)目,觀察控制臺(tái)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

現(xiàn)在,我們已經(jīng)可以通過WebSocket建立一個(gè)連接,下一步將處理用戶認(rèn)證模塊(User Authentication)。記?。何覀兤谕粋€(gè)用戶可以登錄應(yīng)用并且可以看到其他已經(jīng)注冊(cè)的用戶。第一步,創(chuàng)建一個(gè)簡單的用戶登錄界面:
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 %}

更新視圖函數(shù)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帶有支持通用認(rèn)證功能的表單,我們可以使用AuthenticationForm來處理用戶登錄。此表單檢查提供的用戶名和密碼,然后在找到經(jīng)過驗(yàn)證的用戶時(shí)返回一個(gè)用戶對(duì)象。 我們登錄驗(yàn)證的用戶并將其重定向到我們的主頁。 用戶還必須能夠注銷應(yīng)用程序,因此我們創(chuàng)建了一個(gè)注銷視圖,該視圖提供了該功能,然后將用戶重定向至登錄頁面。

更新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')
]

創(chuàng)建一個(gè)注冊(cè)的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è)的視圖函數(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})

為sign_up配置URL:
url(r'^sign_up/$', sign_up, name='sign_up'),

Login Alerts

我們有基本的用戶認(rèn)證功能,但我們?nèi)匀恍枰@示用戶列表,并且我們需要服務(wù)器在用戶登錄和注銷時(shí)告訴用戶組。 重寫consumer函數(shù),使得在客戶端連接之后和在客戶端斷開連接之前立即發(fā)送消息。 消息數(shù)據(jù)將包含用戶的用戶名和連接狀態(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)

我們?cè)诤瘮?shù)中添加了裝飾器以從Django會(huì)話中獲取用戶。 而且,所有消息都必須是JSON序列化的,所以我們將數(shù)據(jù)轉(zhuǎn)儲(chǔ)到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 %}

在主頁上,我們擴(kuò)展用戶列表以顯示用戶信息和在線狀態(tài)。 我們將存儲(chǔ)每個(gè)用戶的用戶名,以便在DOM中查找用戶項(xiàng)。 并且還為WebSocket添加了一個(gè)事件監(jiān)聽器,它可以處理來自服務(wù)器的消息。 當(dāng)收到消息時(shí),解析JSON數(shù)據(jù),找到給定用戶的<li>元素,并更新該用戶的狀態(tài)。

Django不會(huì)記錄用戶是否登錄,所以需要?jiǎng)?chuàng)建一個(gè)簡單的模型來做這件事。 在example_channels / example / models.py中創(chuàng)建一個(gè)LoggedInUser模型,該模型與User模型是一對(duì)一的關(guān)系。

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í)候會(huì)創(chuàng)建一個(gè)LoggedInUser實(shí)例,反之用于注銷時(shí)會(huì)刪除一個(gè)LoggedInUser實(shí)例。

數(shù)據(jù)庫遷移:

$ python manage.py makemigrations
$ python manage.py migrate

接下來,在example_channels / example / views.py中更新我們的user_list視圖,以獲取要呈現(xiàn)的用戶列表:

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相關(guān)聯(lián),那么我們將用戶的狀態(tài)記錄為“Online”,如果不是,則該用戶是“Offline”。 我們還在我們的用戶列表和注銷視圖中添加了@login_required裝飾器,以便僅限注冊(cè)用戶訪問。

此時(shí),用戶可以登錄和注銷,這將觸發(fā)服務(wù)器向客戶端發(fā)送消息,但我們無法知道用戶首次登錄時(shí)哪些用戶登錄。用戶僅在其他用戶 狀態(tài)改變。 這就是LoggedInUser發(fā)揮作用的地方,但我們需要一種方式在用戶登錄時(shí)創(chuàng)建LoggedInUser實(shí)例,然后在用戶注銷時(shí)將其刪除。

Django庫有信號(hào)量的功能,當(dāng)發(fā)生某些操作時(shí)它會(huì)廣播通知。 應(yīng)用程序可以偵聽這些通知,然后對(duì)其執(zhí)行操作。 我們可以利用兩個(gè)有用的內(nèi)置信號(hào)(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----------------------------------------------------

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容