Channels概念

Django的传统概念围绕着HTTP请求和响应展开:服务器收到一个请求,Django调起为其服务,生成响应并发送,然后Django离开、等着下一个请求。

当互联网只包含简单的浏览器行为时,这没什么问题。但现代网络包含了WebSocket和HTTP 2服务器推送等功能,允许网站在传统请求/响应循环之外进行其他通信。

除此之外,还有许多非关键的任务,需要应用程序将其分发出去,在响应发送后继续处理——比如保存缓存,或为新上传的图像生成缩略图。

Channels将Django的运行方式改为“面向事件”——不仅仅响应请求,而是响应通道中发送的大量事件。这里仍然没有持久状态——每个事件处理程序,也可以叫事件消费者,都是以独立的方式调用的,就像调用视图一样。

让我们先看看通道是什么。

通道是什么?

理所当然,核心就是叫做通道的数据结构。通道是什么?它是一个有序的先进先出的队列消息会过期最多发一次一次只发给一个监听应用

可以将其类比为任务队列——生产者将消息发送到一个通道,再发给一个监听该通道的消费者。

最多发一次是指,一条消息要么有一个消费者收到,要么没人收到(例如通道突然崩溃了)。与之对应的是至少一次:正常情况下,一条消息有一个消费者收到,但程序崩溃时,它极有事后可能再重新发送,因此还会有其他消费者重复收到。后者不是我们想要的。

还有其他几个限制——消息必须是可序列化的类型,并且必须保持在一定的大小限制之内——但是在接触更高级的用法之前,无需担心这些实现细节。

通道有容量,即便没有消费者,生产者也可以先向通道中写入大量消息,消费者稍后出现再接收队列中的消息。

如果你使用过Go通道:Go通道与Django通道相当像,关键区别在于Django通道是网络透明的,在不同进程甚至不同计算机上运行的生产者和消费者,都可以通过网络访问到我们的通道。

在网络中,我们通过名称字符串来唯一地标识通道——任何计算机可以将消息发送到任何命名通道,只要它们接入了同一个通道后端。例如两台不同的计算机都写入叫做“http.request”的通道,他们写入的就是同一个通道。

如何使用通道?

那么Django如何使用这些通道呢?在Django中,您可以编写一个函数来使用通道:

def my_consumer(message):
    pass

然后在通道路由中为其分配一个通道:

channel_routing = {
    "some-channel": "myapp.consumers.my_consumer",
}

该通道每收到一条消息,Django都调用这个消费者函数,并传入message参数。message带有content、channel和其他一些属性,其中content属性一定是字典(dict)类型,channel属性用来标明发送消息的通道名称。

Channels将Django从传统的请求/响应模式,改为工作进程模式:监听所有分配了消费者的通道,收到消息就运行对应的消费者。现在,Django不只是在一个连接到WSGI服务器的进程中运行,而是在三个独立的层中运行:

  • 接口服务器,用于Django与外部世界之间的通信。这包括一个WSGI适配器以及一个单独的WebSocket服务器——这在运行接口服务器中进行了介绍。
  • 通道后端,其中包括可扩展的Python代码,以及用来传输消息的数据存储机制(例如Redis或共享内存段)。
  • 工作进程,监听所有相关的通道,并在消息就绪时运行消费者代码。

这或许看起来很简单,但我们就是这样计划的:与其尝试完全异步的架构,不如将现有的Django视图再稍微复杂抽象一点。

A view takes a request and returns a response; a consumer takes a channel message and can write out zero to many other channel messages.

现在,让我们建立一个请求通道(取名http.request),以及面向单个客户端的响应通道(例如http.response.o4f2h2fd),请求通道中的消息带有一个reply_channel属性,对应响应通道的名称。这样,消息消费者就很类似一个视图:

# 监听http.request
def my_consumer(message):
    # 将请求信息从message中解码,生成Request对象
    django_request = AsgiRequest(message)
    # 运行视图
    django_response = view(django_request)
    # 将响应编码为message格式
    for chunk in AsgiHandler.encode_response(django_response):
        message.reply_channel.send(chunk)

这就是通道的机制。接口服务器将外部连接(HTTP、WebSocket等)转换为通道中的消息,你编写工作进程来处理这些消息。正常情况下,普通的HTTP请求还是交给Django内置的消费者,后者将请求传入视图/模板系统,但如果需要,也可以重写它以添加功能。

关键的部分在于,你可以运行代码来响应任何事件——包括你自己创建的事件,运行的代码还可以进一步发消息。你可以在保存Model、其他Views和Forms的代码中触发事件。这样可以方便的写推送程序,例如使用WebSocket或HTTP长轮询来实时通知客户端(比如聊天信息,或者Admin实时查看其他用户编辑更新的内容)。

通道类型

通道主要有两种用途。第一种显而易见,就是把工作分配给消费者——通道中新增一条消息,任何一个工作进程都可以接收消息并运行消费者。

第二种通道用于响应(HTTP、WebSocket)请求。值得注意的是,只有接口服务器才会监听这种通道的消息。每个响应通道名字都不一样,并且必须路由回到特定的接口服务器。

两者区别不大——它们都符合通道的核心定义——但当服务器集群扩大规模时会有问题。对于第一类普通通道,我们可以在通道服务器集群和工作进程中无差别的做负载均衡,任何工作进程都可以通用的处理这些消息。但响应消息只能发送到通道服务器群中、正在监听该响应的通道服务器上。

因此,Channels将其视为两种不同的通道类型,在响应通道名称中包含感叹号!来标识,例如http.response!f5g3fe21f。普通通道名称不包含感叹号。此外,通道名称都只能包含字符a-z A-Z 0-9 - _,并且长度小于200个字符。

对于后端实现来说,不一定要处理这个区别——只有扩大服务器集群规模时,才有必要分别处理这两类通道。有关规模化的更多信息,以及编写后端或接口服务器时如何处理通道类型,请参见:规模化。

分组

通道只能向一个监听方发送消息,不能广播。如果要向一批客户端发送消息,你需要记录所有客户端对应的响应通道。

如果我有一个Liveblog,想在发布新帖子时推送更新,我可以注册一个程序处理post_save信号,同时得记下要发送更新的通道(这个例子使用Redis):

redis_conn = redis.Redis("localhost", 6379)

@receiver(post_save, sender=BlogUpdate)
def send_update(sender, instance, **kwargs):
    # 遍历所有的reply channels,发送更新
    for reply_channel in redis_conn.smembers("readers"):
        Channel(reply_channel).send({
            "text": json.dumps({
                "id": instance.id,
                "content": instance.content
            })
        })

# 连接到websocket.connect
def ws_connect(message):
    # 添加到readers集合
    redis_conn.sadd("readers", message.reply_channel.name)

这段代码可以运行,但还有点小问题——客户端中断连接的时候,我们要记得把他们从readers中删除。为此我们要添加另一个消费者程序,监听和处理websocket.disconnect消息。而且,接口服务器可能遇到强退、断电等情况,来不及发送disconnect信号,你的代码永远收不到disconnect通知,但此时响应通道已经完全失效了,为此我们还需要添加过期机制,发送到响应通道的消息等待一段时间后会过期、被删除,

由于通道的设计基础是无状态的,所以如果接口服务器中断连接,通道服务器也无所谓“关闭”一条通道——通道的任务就是保存消息直到消费者出现(对某些类型的接口服务器比如SMS网关,理论上可以为来自任何接口服务器的任何客户端提供服务)。

我们不太介意断开连接的客户端是不是没收到消息——是它自己断开的——但持续维护这些断开的客户端会让通道后端很乱,累积造成通道名称重复、发错消息,这让我们很介意。

现在,我们回到前面的示例,需要添加过期集合、跟踪过期时间等等,但使用框架的意义就是替你实现这些重复的工作。于是Channels将此抽象实现为一个核心概念,叫做

@receiver(post_save, sender=BlogUpdate)
def send_update(sender, instance, **kwargs):
    Group("liveblog").send({
        "text": json.dumps({
            "id": instance.id,
            "content": instance.content
        })
    })

# 连接到websocket.connect
def ws_connect(message):
    # 添加到readers组
    Group("liveblog").add(message.reply_channel)
    # Accept the connection request
    message.reply_channel.send({"accept": True})

# 连接到websocket.disconnect
def ws_disconnect(message):
    # Remove from reader group on clean disconnect
    Group("liveblog").discard(message.reply_channel)

组不仅有自己的send()方法(后端可以提供高效的实现),而且还自动管理组成员过期——当通道消息没人读而开始过期时,我们找到所有包含该通道的组,将其从中删除。当然,如果能正常收到disconnect消息的话,你还是应该在断开连接时将渠道从组中删除,过期机制是为了解决无法正常收到disconnect消息的情况。

组通常只用于响应通道(包含感叹号!的通道),但如果你愿意,也可以用于普通通道。

下一步

这是对通道和组的高级概述,帮助你形成一些初步概念。要记住,Django提供了一些通道,但您可以自由地创建和使用自己的通道,所有通道都是网络透明的。

另外,通道不保证消息发送成功。如果你需要确保任务完成,请使用专门设计的、带有重试和持久化功能的系统(例如Celery)。你也可以创建管理命令,如果有任务没完成,就再次向通道提交消息(换言之,自己维护重试逻辑)。

我们将在其余文档中更多地介绍什么任务适合用通道,现在让我们进到开始来写些代码。

你可能感兴趣的:(Channels概念)