Django-channels實現websockets

在這個例子中,我們將使用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請求實現客戶端和服務器端的通信:

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

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

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

推薦閱讀更多精彩內容