Django Channels 入门指南

http://www.oschina.NET/translate/in_deep_with_django_channels_the_future_of_real_time_apps_in_djangohtml

 

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

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

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

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

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

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

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

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

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

我认为Channels将是Django的一个很是重要的插件:它们将支撑Django顺利进入这个新的web开发的时代。虽然这些api尚未成为Django的一部分,他们将很快就会是!因此,如今是一个完美的时间开始学习Channels:你能够了解将来的Django。

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

做为一个例子,我构建了一个简单的实时聊天应用程序——就像一个很是很是轻量级的Slack。有不少的房间,每一个人都在同一个房间里能够聊天,彼此实时交互(使用WebSockets)。

你能够访问我在网络上部署的例子,看看在GitHub上的代码,或点击这个按钮来部署本身的。(这须要一个免费的Heroku帐户,因此得要先注册):

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

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

第一步——从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。

接下来咱们作什么

为了搞明白接下来后台须要作些什么,咱们得先看下客户端的代码。你能够在 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。

安装和建立 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的状况。

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应用里使用。

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

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

这里代码不少,让咱们一行行来看:

7. 客户端将会链接到一个/chat/{label}/形式的WebSocket,label映射的是一个房间的属性。由于全部的WebSocket消息(不考虑URL)客户端均可以在相同的频道里发送和获取消息,咱们要在哪一个房间工做,经过路径解析就能够。

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

8. 如今,咱们能够从数据库中查看Room对象了。

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())})

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

最初的几行很简单:从 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,就是这样!

 

部署和扩展

如今咱们已经把 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的免费或者爱好者层上,不过在实际使用环境中你可能须要升级到产品级来提升吞吐量。)

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进程个数是至关的。 

接下来要作些什么呢?

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

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

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

进一步阅读

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

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

相关文章
相关标签/搜索