延伸

Cherrypy是一个真正开放的框架,您可以随意扩展和插入服务器端或按请求的新功能。不管怎样,Cherrypy都可以帮助您构建应用程序,并通过简单的模式支持您的体系结构。

服务器范围的功能

Cherrypy既可以被视为HTTP库,也可以被视为Web应用程序框架。在后一种情况下,它的体系结构提供了支持跨整个服务器实例操作的机制。这提供了一个强大的画布来执行持久的操作,因为服务器范围的函数位于请求处理本身之外。只要公共汽车还活着,它们就可以在整个过程中使用。

典型用例:

  • 保留与外部服务器的连接池,这样您就不必在每次请求时(例如数据库连接)重新打开它们。

  • 后台处理(假设您需要在不阻塞整个请求本身的情况下完成工作)。

发布/订阅模式

Cherrypy的主干包括一个总线系统,它实现了一个简单的 publish/subscribe messaging pattern .简单地说,在奇瑞,一切都是通过那辆公共汽车来控制的。人们可以很容易地把巴士想象成寿司餐厅的腰带,如下图所示。

_images/sushibelt.JPG

您可以订阅和发布到总线上的频道。通道有点像总线中的唯一标识符。当消息发布到一个信道时,总线将把消息发送给该信道的所有订户。

pubsub模式的一个有趣的方面是它促进了调用者和被调用者之间的分离。已发布的消息最终将生成响应,但发布者不知道该响应来自何处。

由于这种分离,Cherrypy应用程序可以轻松地访问功能,而无需保存对提供该功能的实体的引用。相反,应用程序只是发布到总线上,并接收适当的响应,这才是最重要的。

典型模式

让我们采用以下虚拟应用程序:

import cherrypy

class ECommerce(object):
    def __init__(self, db):
        self.mydb = db

    @cherrypy.expose
    def save_kart(self, cart_data):
        cart = Cart(cart_data)
        self.mydb.save(cart)

if __name__ == '__main__':
   cherrypy.quickstart(ECommerce(), '/')

应用程序引用了数据库,但这在数据库提供程序和应用程序之间创建了相当强的耦合。

另一种解决耦合问题的方法是使用pubsub工作流:

import cherrypy

class ECommerce(object):
    @cherrypy.expose
    def save_kart(self, cart_data):
        cart = Cart(cart_data)
        cherrypy.engine.publish('db-save', cart)

if __name__ == '__main__':
   cherrypy.quickstart(ECommerce(), '/')

在这个例子中,我们发布了 cart 实例到 db-save 频道。然后,一个或多个订户可以对该消息作出反应,应用程序不必知道这些消息。

注解

这种方法不是强制性的,而是由您决定如何设计实体交互。

实施细节

Cherrypy的总线实现是简单的,因为它向通道注册函数。每当消息发布到某个通道时,每个已注册的函数都将应用作为参数传递的消息。

整个行为是同步发生的,从这个意义上说,如果订阅服务器处理消息花费的时间太长,那么剩余的订阅服务器将被延迟。

Cherrypy的总线不是一个高级的pubsub消息传递代理系统,如 zeromqRabbitMQ .使用它的前提是它可能有成本。

作为Pubsub总线的引擎

如前所述,Cherrypy是围绕一辆公共巴士建造的。框架在运行时管理的所有实体都在单个总线实例上工作,该实例名为 engine .

因此,总线实现提供了一组描述应用程序生命周期的公共通道:

                 O
                 |
                 V
STOPPING --> STOPPED --> EXITING -> X
   A   A         |
   |    \___     |
   |        \    |
   |         V   V
 STARTED <-- STARTING

状态的转换触发要发布到的通道,以便订户能够对其作出反应。

一个很好的例子是HTTP服务器,它将从 "STOPPED" 向A陈述 "STARTED" 每当消息发布到 start 通道。

内置通道

为了支持其生命周期,Cherrypy定义了一组公共渠道,这些渠道将在不同的州发布:

  • “开始” :当总线在 "STARTING" 状态

  • “主” :定期从Cherrypy的主循环

  • “停止” :当总线在 "STOPPING" 状态

  • “优雅” :总线请求重新加载订阅服务器时

  • “退出” :当总线在 "EXITING" 状态

此频道将由发布到 engine 自动。因此,注册任何需要对 engine .

此外,在请求处理期间,还向发布了一些其他通道。

  • "before_request" :在Cherrypy处理请求之前

  • "after_request" :处理后立即

另外,从 cherrypy.process.plugins.ThreadManager 插件:

  • "acquire_thread"

  • "start_thread"

  • "stop_thread"

  • "release_thread"

总线API

为了使用总线,实现提供了以下简单的API:

  • 这个 channel 参数是一个字符串,用于标识消息应发送到的通道

  • *args 是消息,可以包含任何有效的python值或对象。

  • 这个 channel 参数是一个字符串,用于标识 callable 将注册到。

  • callable 是签名必须与将要发布的内容匹配的python函数或方法。

  • 这个 channel 参数是一个字符串,用于标识 callable 已注册到。

  • callable 是注册的python函数或方法。

插件

简单地说,插件是通过发布或订阅频道来使用总线的实体,通常两者同时使用。

重要

插件在您具有以下功能时非常有用:

  • 整个应用服务器都可用

  • 与应用程序的生命周期关联

  • 您希望避免与应用程序强耦合

创建插件

一个典型的插件如下所示:

import cherrypy
from cherrypy.process import wspbus, plugins

class DatabasePlugin(plugins.SimplePlugin):
    def __init__(self, bus, db_klass):
        plugins.SimplePlugin.__init__(self, bus)
        self.db = db_klass()

    def start(self):
        self.bus.log('Starting up DB access')
        self.bus.subscribe("db-save", self.save_it)

    def stop(self):
        self.bus.log('Stopping down DB access')
        self.bus.unsubscribe("db-save", self.save_it)

    def save_it(self, entity):
        self.db.save(entity)

这个 cherrypy.process.plugins.SimplePlugin 是Cherrypy提供的帮助程序类,它将自动订阅 startstop 相关渠道的方法。

startstop 频道在上发布,这些方法将相应地调用。

注意我们的插件如何订阅 db-save 这样总线就可以向插件发送消息。

启用插件

要启用该插件,必须将其注册到总线,如下所示:

DatabasePlugin(cherrypy.engine, SQLiteDB).subscribe()

这个 SQLiteDB 这里有一个假类,用作我们的数据库提供程序。

禁用插件

还可以按如下方式注销插件:

someplugin.unsubscribe()

当您希望阻止Cherrypy启动默认HTTP服务器时,通常会使用此选项,例如,如果您在不同的HTTP服务器(支持wsgi)上运行:

cherrypy.server.unsubscribe()

让我们看一个使用这个默认应用程序的例子:

import cherrypy

class Root(object):
    @cherrypy.expose
    def index(self):
        return "hello world"

if __name__ == '__main__':
    cherrypy.quickstart(Root())

例如,运行此应用程序时,您将看到:

[27/Apr/2014:13:04:07] ENGINE Listening for SIGHUP.
[27/Apr/2014:13:04:07] ENGINE Listening for SIGTERM.
[27/Apr/2014:13:04:07] ENGINE Listening for SIGUSR1.
[27/Apr/2014:13:04:07] ENGINE Bus STARTING
[27/Apr/2014:13:04:07] ENGINE Started monitor thread 'Autoreloader'.
[27/Apr/2014:13:04:08] ENGINE Serving on http://127.0.0.1:8080
[27/Apr/2014:13:04:08] ENGINE Bus STARTED

现在让我们取消订阅HTTP服务器:

import cherrypy

class Root(object):
    @cherrypy.expose
    def index(self):
        return "hello world"

if __name__ == '__main__':
    cherrypy.server.unsubscribe()
    cherrypy.quickstart(Root())

这就是我们得到的:

[27/Apr/2014:13:08:06] ENGINE Listening for SIGHUP.
[27/Apr/2014:13:08:06] ENGINE Listening for SIGTERM.
[27/Apr/2014:13:08:06] ENGINE Listening for SIGUSR1.
[27/Apr/2014:13:08:06] ENGINE Bus STARTING
[27/Apr/2014:13:08:06] ENGINE Started monitor thread 'Autoreloader'.
[27/Apr/2014:13:08:06] ENGINE Bus STARTED

如您所见,服务器没有启动。失踪者:

[27/Apr/2014:13:04:08] ENGINE Serving on http://127.0.0.1:8080

按请求函数

Web应用程序开发中最常见的任务之一是根据运行时上下文调整请求的处理。

在Cherrypy中,这是通过所谓的 工具 .如果您熟悉django或wsgi中间件,那么cherrypy工具在精神上是相似的。它们添加在请求/响应处理期间应用的函数。

钩点

挂接点是请求/响应处理过程中的一个点。

下面是您可以挂起工具的“钩子点”的简要说明:

  • "on_start_resource" -最早的钩子;请求行和请求头已经被处理,调度器已经设置了request.handler和request.config。

  • "before_request_body" -这里连接的工具在处理请求主体之前运行。

  • "before_handler" -就在request.handler之前 exposed 调用调度器找到的可调用项)。

  • "before_finalize" -这个钩子在页面处理程序被处理之后和Cherrypy格式化最终响应对象之前被调用。例如,它可以帮助您检查页面处理程序可能返回的内容,并根据需要更改一些标题。

  • "on_end_resource" -处理完成-已准备好返回响应。这并不总是意味着request.handler(公开的页处理程序)已经执行!可能是发电机。如果您的工具绝对需要在页面处理程序生成响应主体之后运行,那么您需要改为在请求时使用,或者将response.body包装在生成响应主体时应用工具的生成器中。

  • "before_error_response" -在设置错误响应(状态代码、正文)之前立即调用。

  • "after_error_response" -在设置错误响应(状态代码、正文)之后和错误响应完成之前立即调用。

  • "on_end_request" -请求/响应对话结束,所有数据都已写入客户端,此处不再显示任何内容,请继续。

工具

工具是一个简单的可调用对象(函数、方法、实现 __call__ 方法)附加到 hook point .

下面是一个附加到 before_finalize 挂接点,因此在调用页处理程序之后:

@cherrypy.tools.register('before_finalize')
def logit():
   print(cherrypy.request.remote.ip)

还可以手动创建和分配工具。装饰师注册相当于:

cherrypy.tools.logit = cherrypy.Tool('before_finalize', logit)

使用该工具简单如下:

class Root(object):
    @cherrypy.expose
    @cherrypy.tools.logit()
    def index(self):
        return "hello world"

显然,该工具可以声明为 other usual ways .

注解

工具的名称,从技术上讲,属性设置为 cherrypy.tools ,不必与可调用文件的名称匹配。但是,在配置中将使用该名称来引用该工具。

有状态工具

工具机制非常灵活,支持丰富的按请求功能。

如前一节所示的直工具通常足够好。但是,如果您的工作流在请求处理期间需要某种状态,您可能需要基于类的方法:

import time

import cherrypy

class TimingTool(cherrypy.Tool):
    def __init__(self):
        cherrypy.Tool.__init__(self, 'before_handler',
                               self.start_timer,
                               priority=95)

    def _setup(self):
        cherrypy.Tool._setup(self)
        cherrypy.request.hooks.attach('before_finalize',
                                      self.end_timer,
                                      priority=5)

    def start_timer(self):
        cherrypy.request._time = time.time()

    def end_timer(self):
        duration = time.time() - cherrypy.request._time
        cherrypy.log("Page handler took %.4f" % duration)

cherrypy.tools.timeit = TimingTool()

此工具计算页面处理程序为给定请求所花费的时间。它存储处理程序将要被调用的时间,并在处理程序返回其结果后立即记录时间差。

导入位是 cherrypy.Tool 构造函数允许您注册到一个钩子点,但要将同一个工具附加到另一个钩子点,必须使用 cherrypy.request.hooks.attach 方法。这个 cherrypy.Tool._setup 当工具应用于请求时,CherryPy会自动调用方法。

接下来,让我们看看如何使用我们的工具:

class Root(object):
    @cherrypy.expose
    @cherrypy.tools.timeit()
    def index(self):
        return "hello world"

工具排序

因为您可以在同一个钩子点注册许多工具,所以您可能会想知道它们将按什么顺序应用。

Cherrypy提供了一种确定性的,但又很简单的机制。只需设置 优先 属性值从1到100,值越小优先级越高。

如果为多个工具设置相同的优先级,则将按照在配置中声明它们的顺序调用它们。

工具盒

所有内置的cherrypy工具都被收集到一个名为 cherrypy.tools .它响应中的配置项 "tools" namespace .您可以将自己的工具添加到此工具箱中,如上所述。

如果需要更模块化,您还可以自己制作工具箱。例如,您可以创建多个用于使用JSON的工具,或者发布一组涉及身份验证和授权的工具,每个人都可以从中受益(提示、提示)。创建一个新工具箱非常简单:

import cherrypy

# Create a new Toolbox.
newauthtools = cherrypy._cptools.Toolbox("newauth")

# Add a Tool to our new Toolbox.
@newauthtools.register('before_request_body')
def check_access(default=False):
    if not getattr(cherrypy.request, "userid", default):
        raise cherrypy.HTTPError(401)

然后,在应用程序中,像使用一样使用它 cherrypy.tools 在应用程序中注册工具箱的附加步骤。请注意,这样做会自动注册 "newauth" config namespace;您可以在下面的操作中看到config条目:

import cherrypy

class Root(object):
    @cherrypy.expose
    def default(self):
        return "Hello"

conf = {
   '/demo': {
       'newauth.check_access.on': True,
       'newauth.check_access.default': True,
    }
}

app = cherrypy.tree.mount(Root(), config=conf)

请求参数操作

HTTP使用字符串在两个端点之间传输数据。但是,您的应用程序可以更好地使用更丰富的对象类型。由于让每个页面处理程序对数据进行反序列化既不是真正可读的,也不是维护方面的好主意,因此将此函数委托给工具是一种常见的模式。

例如,假设查询字符串中有一个用户ID,数据库中存储了一些用户数据。您可以检索数据,创建一个对象并将其传递给页面处理程序,而不是用户ID。

import cherrypy

class UserManager(cherrypy.Tool):
    def __init__(self):
        cherrypy.Tool.__init__(self, 'before_handler',
                               self.load, priority=10)

    def load(self):
        req = cherrypy.request

        # let's assume we have a db session
        # attached to the request somehow
        db = req.db

        # retrieve the user id and remove it
        # from the request parameters
        user_id = req.params.pop('user_id')
        req.params['user'] = db.get(int(user_id))

cherrypy.tools.user = UserManager()


class Root(object):
    @cherrypy.expose
    @cherrypy.tools.user()
    def index(self, user):
        return "hello %s" % user.name

换言之,Cherrypy赋予您以下能力:

  • 将不属于初始请求的数据注入到页面处理程序中

  • 同时删除数据

  • 将数据转换为另一个更有用的对象,以从页处理程序本身中消除这种负担。

定制调度员

分派是为给定请求定位适当的页面处理程序的艺术。通常,调度基于请求的URL、查询字符串,有时还基于请求的方法(get、post等)。

基于此,Cherrypy已经配备了各种调度员。

然而,在某些情况下,您需要更多。下面是一个调度器示例,它将始终确保传入的URL指向小写的页面处理程序。

import random
import string

import cherrypy
from cherrypy._cpdispatch import Dispatcher

class StringGenerator(object):
   @cherrypy.expose
   def generate(self, length=8):
       return ''.join(random.sample(string.hexdigits, int(length)))

class ForceLowerDispatcher(Dispatcher):
    def __call__(self, path_info):
        return Dispatcher.__call__(self, path_info.lower())

if __name__ == '__main__':
    conf = {
        '/': {
            'request.dispatch': ForceLowerDispatcher(),
        }
    }
    cherrypy.quickstart(StringGenerator(), '/', conf)

运行此代码段后,请转到:

在这两种情况下,您将被引导到 generate 页面处理程序。如果没有我们自制的调度员,第二个调度员会失败并返回404错误 (RFC 7231#section-6.5.4

工具还是调度员?

在前面的例子中,为什么不简单地使用一个工具呢?好吧,找到页面处理程序后,调用工具的时间越早。在我们的示例中,已经太迟了,因为默认调度器甚至找不到匹配的 /GENerAte .

调度器的存在主要是为了确定为请求的资源提供服务的最佳页面处理程序。

另一方面,有一些工具可以使请求的处理适应应用程序的运行时上下文和请求的内容。

通常,只有当您有一个非常具体的用例来定位最合适的页面处理程序时,才需要编写一个调度器。否则,默认的就足够了。

请求正文处理器

自3.2版本发布以来,Cherrypy提供了一种非常优雅和强大的机制,以基于其mimetype处理请求的主体。请参阅 cherrypy._cpreqbody 模块了解如何实现自己的处理器。