zoukankan      html  css  js  c++  java
  • 实时 Django 终于来了 —— Django Channels 入门指南

    Reference: http://www.oschina.net/translate/in_deep_with_django_channels_the_future_of_real_time_apps_in_django

    今天,我们很高兴请到Jacob Kaplan-Moss。Jacob是来自Herokai,也是 Django的长期的核心代码贡献者,他将在这里分享一些他对某些特性的深入研究,他认为这些特性将重新定义框架未来。

    当Django刚创建时,那是十多年前,网络还是一个不太复杂的地方。大部分的网页都是静态的。由数据库支撑的模型/视图/ 控制器架构的网络应用还是很新鲜的东西。Ajax刚刚开始被使用,只在较少的场景中。

    到现在2016年,网络明显更加强大。过去的几年里已经看到了所谓的“实时”网络应用:在这类应用中客户端和服务器之间、点对点通信交互非常频繁。包含很多服务(又名微服务)的应用也变成是常态。新的web技术允许web应用程序走向十年前我们只敢在梦里想象的方向。这些核心技术之一就是WebSockets:一种新的提供全双工通信的协议——一个持久的,允许任何时间发送数据的客户端和服务器之间的连接。

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    2人顶

     

     翻译的不错哦!

    在这个新的世界,Django显示出了它的老成。在其核心,Django是建立在请求和响应的简单概念之上的:浏览器发出请求,Django调用一个视图,它返回一个响应并发送回浏览器。

    这在WebSockets中是行不通的 !视图的生命周期只在一个请求当中,没有一种机制能打开一个连接不断的发送数据到客户端,而不发送相关请求。

    因此:Django  Channels就应运而生了。Channels,简而言之,取代了Django中的“guts” ——请求/响应周期发送跨通道的消息。Channels允许Django以非常类似于传统HTTP的方式支持WebSockets。Channels也允许在运行Django的服务器上运行后台任务。HTTP请求表现以前一样,但也通过Channels进行路由。因此,在Channels 支持下Django现在看起来像这样:

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    2人顶

     

     翻译的不错哦!

    如您所见,Django Channels引入了一些新的概念:

    Channels基本上就是任务队列:消息被生产商推到通道,然后传递给监听通道的消费者之一。如果你使用Go语言中的渠道,这个概念应该相当熟悉。主要的区别在于,Django Channels通过网络工作,使生产者和消费者透明地运行在多台机器上。这个网络层称为通道层。通道设计时使用Redis作为其首选通道层,虽然也支持其他类型(和API来创建自定义通道层)。有很多整洁和微妙的技术细节,查阅文档可以看到完整的记录。

    现在,通道作为一个独立的应用程序搭配使用Django 1.9使用。计划是将通道合并到Django1.10版本,今年夏天将会发布。

    我认为Channels将是Django的一个非常重要的插件:它们将支撑Django顺利进入这个新的web开发的时代。虽然这些api还没有成为Django的一部分,他们将很快就会是!所以,现在是一个完美的时间开始学习Channels:你可以了解未来的Django。

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    1人顶

     

     翻译的不错哦!

    开始实践:如何在Django中实现一个实时聊天应用

    作为一个例子,我构建了一个简单的实时聊天应用程序——就像一个非常非常轻量级的Slack。有很多的房间,每个人都在同一个房间里可以聊天,彼此实时交互(使用WebSockets)。

    你可以访问我在网络上部署的例子,看看在GitHub上的代码,或点击这个按钮来部署自己的。(这需要一个免费的Heroku账户,所以得要先注册):

    注意:你需要在点击上面的按钮后,启动工作进程。使用仪表盘或运行heroku ps:scale web=1:free worker=1:free。

    如果你想深入了解这个应用程序是如何工作的——包括你为什么需要worker!——那么请继续读下去。我将会一步一步来构建这个应用程序,并突出关键位置和概念。

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    1人顶

     

     翻译的不错哦!

    第一步——从Django开始

    虽然在实现上有了很大差异,但是这仍旧是我们使用了十年的Django。所以第一步和其他任何Django应用是一样的(如果你是Django新手,你得看看如何在Heroku上开始使用PythonDjango新手教程)。创建一个工程后,你可以定义模型来表示一个聊天室和其中的消息(chat/models.py):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class Room(models.Model):
        name = models.TextField()
        label = models.SlugField(unique=True)
     
    class Message(models.Model):
        room = models.ForeignKey(Room, related_name='messages')
        handle = models.TextField()
        message = models.TextField()
        timestamp = models.DateTimeField(default=timezone.now, db_index=True)

    (在这一步中,包括后面的例子,我已经将代码最简化,希望能将焦点放到重点上,全部代码请看Gitbub。)

    然后创建一个聊天室视图以及相应的urls.py模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    def chat_room(request, label):
        # If the room with the given label doesn't exist, automatically create it
        # upon first visit (a la etherpad).
        room, created = Room.objects.get_or_create(label=label)
     
        # We want to show the last 50 messages, ordered most-recent-last
        messages = reversed(room.messages.order_by('-timestamp')[:50])
     
        return render(request, "chat/room.html", {
            'room': room,
            'messages': messages,
        })

    现在,我们已经已经有了一个可以运行的Django应用。如果你在标准的Django环境中运行它,你可以看到已经存在的聊天室和聊天记录,但是聊天室内无法进行交互操作。实时没有起作用,我们得做工作来处理 WebSockets。

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    1人顶

     

     翻译的不错哦!

    接下来我们做什么

    为了搞明白接下来后台需要做些什么,我们得先看下客户端的代码。你可以在 chat.js 中找到,其实也没做多少工作!首先,创建一个 websocket:

    1
    2
    var ws_scheme = window.location.protocol == "https:" "wss" "ws";
    var chat_socket = new ReconnectingWebSocket(ws_scheme + '://' + window.location.host + "/chat" + window.location.pathname);

    注意:

    接下来,我们将加入一个回调函数,当表单提交时,我们就通过WebSocket发送数据(而不是 POST数据):

    1
    2
    3
    4
    5
    6
    7
    8
    $('#chatform').on('submit', function(event) {
        var message = {
            handle: $('#handle').val(),
            message: $('#message').val(),
        }
        chat_socket.send(JSON.stringify(message));
        return false;
    });

    我们可以通过WebSocket发送任何想要发送的数据。像众多的API一样, JSON 是最容易的,所以我们将要发送的数据打包成JSON格式。

    最后,我们需要将回调函数与WebSocket上的新数据接收事件对接起来:

    1
    2
    3
    4
    5
    6
    7
    8
    chatsock.onmessage = function(message) {
        var data = JSON.parse(message.data);
        $('#chat').append('<tr>' 
            + '<td>' + data.timestamp + '</td>' 
            + '<td>' + data.handle + '</td>'
            + '<td>' + data.message + ' </td>'
        + '</tr>');
    };

    简单提示:从获取的信息中拉取数据,在会话的表上加上一行。如果现在就运行这个代码,他是无法运行的,现在还没有谁监听WebSocket连接呢,只是简单的HTTP。现在,让我们来连接WebSocket。

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    1人顶

     

     翻译的不错哦!

    安装和创建 Channels

    要将这个应用“通道化”,我们需要做三件事情:安装Channels,建立通道层,定义通道路由,修改我们的工程使其运行在Channels上(而不是WSGI)。

    1. 安装Channels

    要安装Channels,只需要执行pip install channels,然后将 "channels”添加到 INSTALLED_APPS配置项中。安装Channels后,允许Django以“通道模式”运行,使用上面描述的通道架构来完成请求/响应的循环。(为了向后兼容,你仍可以以 WSGI模式运行Django ,但是在这种模式下WebSockets和Channel的其他特性就不能工作了。)

    2. 选择一个通道层

    接下来,我们将定义一个通道层。这是Channels用来在消费者和生产者(消息发送者)之间传递消息的交换机制。 这是一种有特定属性的消息队列(详细信息请查看Channels文档)。

    我们将使用Redis作为我们的通道层:它是首选的生产型(可用于工程部署)通道层,是部署在Heroku上显而易见的选择。 当然也有一些驻留内存和基于数据的通道层,但是它们更适合于本地开发或者低流量情况下使用。 (更多细节,再次请查看 文档。)

    但是首先:因为Redis通道层是在另外的包中实现的,我们需要运行pip安装 asgi_redis。(我将会在下面稍微介绍点“ASGI”。)然后我们在CHANNEL_LAYERS配置中定义通道层:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    CHANNEL_LAYERS = {
        "default": {
            "BACKEND""asgi_redis.RedisChannelLayer",
            "CONFIG": {
                "hosts": [os.environ.get('REDIS_URL''redis://localhost:6379')],
            },
            "ROUTING""chat.routing.channel_routing",
        },
    }

    要注意的是我们把Redis的连接URL放到环境外面,以适应部署到Heroku的情况。

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    1人顶

     

     翻译的不错哦!

    3. 通道路由

    在通道层(CHANNEL_LAYERS),我们已经告诉 Channel去哪里找通道路由——chat.routing.channel_routing。通道路由很类似与URL路由的概念:URL路由将URL映射到视图函数;通道路由将通道映射到消费者函数。跟 urls.py类似,按照惯例通道路由应该在routing.py里。现在,我们创建一条空路由:

    1
    channel_routing = {}

    (我们将在后面看到好几条通道路由信息,当连接WebSocket的时候回用到。)

    你会注意到我们的app里有urls.py和routing.py两个文件:我们使用同一个app处理HTTP请求和WebSockets。这是很典型的做法:Channels应用也是Django应用,所以你想用的所有Django的特性——视图,表单,模型等等——都可以在Channels应用里使用。

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    1人顶

     

     翻译的不错哦!

    4. 运行

    最后,我们需要替换掉Django的基于HTTP/WSGI的请求处理器,而是使用通道。它是一个基于新兴标准ASGI(异步服务器网关接口)的, 所以我们将在asgi.py文件里定义处理器:

    1
    2
    3
    4
    5
    import os
    import channels.asgi
     
    os.environ.setdefault("DJANGO_SETTINGS_MODULE""chat.settings")
    channel_layer = channels.asgi.get_channel_layer()

    (将来,Django会自动生成这个文件,就像现在自动生成wsgi.py文件一样。)

    现在,如果一切顺利的话,我们应该能在通道上把这个app运行起来。Channels接口服务叫做Daphne,我们可以运行如下命令运行这个app:

    1
    $ daphne chat.asgi:channel_layer --port 8888

    ** 如果现在访问http://localhost:8888/ 我们会看到……什么事情也没发生。这很让人困惑,直到你想起Channels将 Django分成了两部分:前台接口服务 Daphne,后台消息消费者。所以想要处理HTTP 请求,我们得运行一个worker:

    1
    $ python manage.py runworker

    现在请求应该能传递过去了。这说明了其中的机制很简洁:Channels 继续处理 HTTP(S)请求,但是是以一个完全不同的方式去处理,这与通过Django运行 Celery 没有太大的不同,那种情况下运行WSGI服务的同时也要运行Celery服务。不过现在,所有的任务——HTTP请求, WebSockets,后台服务都在worker中运行起来了.

    (顺便说一句,我们仍然可以通过运行python manage.py runserver命令来做本地测试。当这么做时, Channels只是在同一进程里运行起Daphne和一个worker。)

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    1人顶

     

     翻译的不错哦!

    WebSocket消费者

    好了,我们已经完成了安装;让我们开始进入最奇妙的部分吧。

    Channels 将WebSocket连接映射到三个通道中:

    • 一个新的客户端 (如浏览器)第一次通过WebSocket连接上时,一条消息被发送到 websocket.connect 通道。当这发生时,我们记录这个客户端当前进入一个已知的聊天室。

    • 每条客户端通过已建立的socket发送的消息都被发送到 websocket.receive通道。(这些都是从浏览器接收到的消息;记住通道都是单向的。我们等一会儿会介绍如何将消息发送给客户端。)当一条消息被接受时,我们将对聊天室里所有其他客户端进行广播。

    • 最后,当客户端断开连接时,一条消息被发送到websocket.disconnect通道。当这发生时,我们将此客户端从聊天室里移除。

    首先,我们得在routing.py文件里对这个三个通道进行hook:

    1
    2
    3
    4
    5
    6
    7
    from import consumers
     
    channel_routing = {
        'websocket.connect': consumers.ws_connect,
        'websocket.receive': consumers.ws_receive,
        'websocket.disconnect': consumers.ws_disconnect,
    }

    其实很简单:就是将每个通道连接到对应的处理函数。现在我们来看看这些函数。按照惯例我们会将这些函数放到一个 consumers.py 文件里(但是像视图一样,其实也可以放在任何地方)。

    首先来看看 ws_connect:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    from channels import Group
    from channels.sessions import channel_session
    from .models import Room
     
    @channel_session
    def ws_connect(message):
        prefix, label = message['path'].strip('/').split('/')
        room = Room.objects.get(label=label)
        Group('chat-' + label).add(message.reply_channel)
        message.channel_session['room'= room.label

    (为了清晰起见,我将代码中的异常处理和日志去掉了。要看完整版本,请看GitHub上的consumers.py)。

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    3人顶

     

     翻译的不错哦!

    这里代码很多,让我们一行行来看:

    7. 客户端将会连接到一个/chat/{label}/形式的WebSocket,label映射的是一个房间的属性。因为所有的WebSocket消息(不考虑URL)客户端都可以在相同的频道里发送和获取消息,我们要在哪个房间工作,通过路径解析就可以。

    客户端解析WebSocket路径是通过读取message['path']获得的,这不同于传统的URL路由,Django的urls.py的路由是基于path的。如果你有多个WebSocket URL,你会需要路由到你自己定制的不同函数。(这是一个“早期”频道方面的内容;很可能在未来的版本里Channel将会包含在WebSocket URL 路由中。)

    8. 现在,我们可以从数据库中查看Room对象了。

    无若
    无若
    翻译于 4个月前

    1人顶

     

     翻译的不错哦!

    9. 这条线是使聊天功能能工作的关键。我们需要知道如何把消息发送回这个客户端。要做到这点,我们将使用消息的应答通道——每条消息都会有一个应答通道属性(reply_channelattribute),可以用来把消息发送回这个客户端。(我们不需要去自己创建这个通道;Channels已经创建好了。)

    然而,只把消息发送到这一个通道还是远远不够的的;当一个用户聊天时,我们想把消息送给每一个连接到此聊天室的用户。要做到这点,我们使用一个通道组(channel group)。一个组是由多个通道连接而成,你可以用他来广播消息。所以,我们将这个消息的应答通道加入到这个聊天室的特殊通道组中。

    10. 最后,后续的消息(接收/断开)不再包含这个URL(因为连接已经激活)。所以,我们需要一种方式来把一个WebSocket连接映射到哪个聊天室记录下来。要做到这点,我们可以使用一个通道会话。通道会话很像 Django的会话框架: 它们通过通道消息的属性message.channel_session把这些信息持久化下来。我们给一个消费者添加修饰属性 @channel_session,就可以让会话框架起效。 (文档见 通道会话如何工作的更多细节)。

    现在一个客户端已经连接上来了,让我们看看ws_receive。WebSocket上每接收一条消息,这个消费者都会被调用:

    1
    2
    3
    4
    5
    6
    7
    @channel_session
    def ws_receive(message):
        label = message.channel_session['room']
        room = Room.objects.get(label=label)
        data = json.loads(message['text'])
        = room.messages.create(handle=data['handle'], message=data['message'])
        Group('chat-'+label).send({'text': json.dumps(m.as_dict())})

    (再一次说明,为了清晰起见,我把错误处理和日志都去掉了。)

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    1人顶

     

     翻译的不错哦!

    最初的几行很简单:从 channel_session中解析出聊天室,在数据库中查找出来该聊天室,解析JSON消息,将消息作为Message对象存放在数据库中。然后,我们所要作的就是将这条消息广播给聊天室里所有的成员,为了做到这点我们可以使用和前面一样的通道组。Group.send()将会把这条信息发送到加入到本组的所有reply_channel。

    然后, ws_disconnect就很简单了:

    1
    2
    3
    4
    @channel_session
    def ws_disconnect(message):
        label = message.channel_session['room']
        Group('chat-'+label).discard(message.reply_channel)

    这里,在从channel session里查找到聊天室后,我们从聊天组里断开了reply_channel,就是这样!

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    2人顶

     

     翻译的不错哦!

    部署和扩展

    现在我们已经把 WebSockets连接起来并开始工作,我们可以像上面一样运行daphne和worker进行测试,或者运行manage.py runserver)。但是和自己聊天是很寂寞的哦,所以让我们在Heroku上把它跑起来!

    大部分情况下, 一个 Channels 应用和一个Python应用在Heroku上都是一样的——在requirements.txt中有详细需求, 在runtime.txt定义Python运行事,通过标准的git推送到heroku上进行部署,等等。 (对于一个新手,请看 在Heroku上开始Python开发教程。) 我将重点突出那些Channel应用和标准Django应用不一样的地方:

    1. Procfile 和处理类型

    因为Channels应用同时需要 HTTP/WebSocket 服务和一个后台通道消费者, 所以Procfile需要定义这两种类型。下面是我们的Procfile:

    1
    2
    web: daphne chat.asgi:channel_layer --port $PORT --bind 0.0.0.0 -v2
    worker: python manage.py runworker -v2

    当我们首次部署,我们需要确认两种处理类型都在运行中(Heroku默认值启动web进程):

    1
    $ heroku ps:scale web=1:free worker=1:free

    (一个简单的应用将运行在 Heroku的免费或者爱好者层上,不过在实际使用环境中你可能需要升级到产品级来提高吞吐量。)

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    2人顶

     

     翻译的不错哦!

    2. 插件: Postgres和Redis

    就像Django的大多数应用,你需要一个数据库, Heroku的Postgres可以完美的满足要求。然而,Channels也需要一个 Redis实例作为通道层。所以,我们在首次部署我们的应用时需要创建一个 Heroku Postgres和一个 Heroku Redis:

    $ heroku addons:create heroku-postgresql
    $ heroku addons:create heroku-redis

    3. 扩展

    因为Channels实在是太新了,扩展性问题还不是很了解。然而,基于现在的架构和我早前做的一些性能测试,我可以做出一些预测。关键点在于Channels 把负责连接的处理进程(daphne)和负责通道消息处理的处理进程(runworker)分开了。这意味着:

    • 通道的吞吐量——HTTP请求, WebSocket消息,或者自定义的通道消息——取决于工作者进程的数量。所以,如果你需要处理大量的请求,你可以扩展工作者进程 (比如,heroku上 ps:scale worker=3)。

    • 并发水平——当前打开的连接数——将受限于前端web进程的规模。所以,如果你需要处理大量并发的WebSocket连接,你得扩展web进程(比如, heroku 上ps:scale worker=2)。

    基于我前期做的测试工作, 在一个Standard-1X进程内Daphne是非常适合处理成百的并发连接的。所以我估计很少有场景需要扩展这个web进程。一个Channels应用中的工作者进程的个数与一个老风格Django应用所需的web进程个数是相当的。 

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    1人顶

     

     翻译的不错哦!

    接下来要做些什么呢?

    对WebSocket的支持是Django的一项很大的新特性,但是这只粗浅介绍了Channels可以做些什么。你要记住:Channels是一个运行后台任务的通用工具。因此,很多过去需要 Celery 或者 Python-RQ 才能做得事情,都可以用Channels替换。 Channels无法完全替换复杂的任务队列:他有些很重要的限制,比如只发一次,这并不适合所有的场景。 查看文档以了解全部细节。 当然, Channels可以使通常的后台任务更加简单。比如,你可以很容易的使用Channels完成图像缩略图生成,发送邮件、推文或者短信,运行耗时数据计算等等工作。

    对于Channels来说:计划在 Django 1.10中包含Channels ,定于今年夏天发布。这意味着现在是一个很好的时机来尝试一下并给出反馈:您的反馈将会推动这一重要特性的发展方向。如果你想参与进来,看看这份指导文档向Djang贡献代码,  然后到 django开发者邮件列表 里分享你的反馈。

    最后: 非常感谢 Andrew Godwin 在 Channels上付出的努力工作。这真是Django的一个非常激动人心的新方向,我很激动地看到它开始发展起来。

    雪落无痕xdj
    雪落无痕xdj
    翻译于 4个月前

    2人顶

     

     翻译的不错哦!

    进一步阅读

    关于Channels的更多信息,请查看Channels文档,其中包含很多细节和引用,包括:

    关于在 Heroku上使用Python 的信息,请访问Python on Heroku in Dev Center。我推荐其中的几篇特别好的文章:

  • 相关阅读:
    Composite in Javascript
    Model Validation in Asp.net MVC
    HttpRuntime.Cache vs. HttpContext.Current.Cache
    Controller Extensibility in ASP.NET MVC
    The Decorator Pattern in Javascript
    The Flyweight Pattern in Javascript
    Model Binding in ASP.NET MVC
    Asp.net MVC
    jQuery Ajax 实例 全解析
    ASP.NET AJAX入门系列
  • 原文地址:https://www.cnblogs.com/skying555/p/5698115.html
Copyright © 2011-2022 走看看