Django Channels 入門

原文: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-noWorkerpythonManage.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 項目的文檔,看看你還可以用它來實現什么有趣的東東。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,786評論 6 534
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,656評論 3 419
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,697評論 0 379
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,098評論 1 314
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,855評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,254評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,322評論 3 442
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,473評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,014評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,833評論 3 355
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,016評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,568評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,273評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,680評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,946評論 1 288
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,730評論 3 393
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,006評論 2 374

推薦閱讀更多精彩內容

  • 版權: https://github.com/haiiiiiyun/awesome-django-cn Aweso...
    若與閱讀 23,158評論 3 241
  • 在這個例子中,我們將使用Django Channels來創建一個實時在線應用,當用戶登錄或下線時,這個應用可以自動...
    Ccccolin_aha閱讀 13,973評論 1 12
  • Spring Web MVC Spring Web MVC 是包含在 Spring 框架中的 Web 框架,建立于...
    Hsinwong閱讀 22,502評論 1 92
  • Spring Cloud為開發人員提供了快速構建分布式系統中一些常見模式的工具(例如配置管理,服務發現,斷路器,智...
    卡卡羅2017閱讀 134,787評論 18 139
  • 模塊間聯系越多,其耦合性越強,同時表明其獨立性越差( 降低耦合性,可以提高其獨立性)。軟件設計中通常用耦合度和內聚...
    riverstation閱讀 2,085評論 0 8