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 href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" rel="stylesheet">
  <title>Example Channels</title>
</head>
<body>
  <div class="container">
    <br>
    {% block content %}{% endblock content %}
  </div>
  <script src="//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阅读 203,456评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,370评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,337评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,583评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,596评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,572评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,936评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,595评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,850评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,601评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,685评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,371评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,951评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,934评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,167评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,636评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,411评论 2 342

推荐阅读更多精彩内容