码迷,mamicode.com
首页 > 其他好文 > 详细

tornado路由(Routing)

时间:2015-05-24 12:59:24      阅读:1421      评论:0      收藏:0      [点我收藏+]

标签:python   tornado   web   框架   

url路由

tornado代码文档中提到,“A collection of request handlers that make up a web application“。其实更加detail一点的说法应该是,”A collection of request handlers and a url route talbe that make up a web application”。一个web应用是由一个路由表和请求处理器集合组成。

我们注意到,概念上的抽象,web application的底层才是http server(另外一个著名的python开发框架django也采取同样的概念,命令”python manage.py startapp blog”,生成一个web application)。

其实路由表具有非常重要的作用,卸耦了http server层和web application层

在web框架中,路由表中的任意一项是一个元组,每个元组包含pattern(模式)和handler(处理器)。当httpserver接收到一个http请求,server从接收到的请求中解析出url path(http协议start line中),然后顺序遍历路由表,如果发现url path可以匹配某个pattern,则将此http request交给web应用中对应的handler去处理。

由于有了url路由机制,web应用开发者不必和复杂的http server层代码打交道,只需要写好web应用层的逻辑(handler)即可。

Application

Application类位于web.py,一个Application类的实例相当于一个web应用。前面提到一个web应用是由路由表和处理器(hanlders)组成。当然存储路由表最合适的地方是Application类的实例。

application = tornado.web.Application([
        (r"/", MainHandler),
    ])
    http_server = tornado.httpserver.HTTPServer(application)
    http_server.listen(options.port)
    tornado.ioloop.IOLoop.current().start()

上面的代码,把一个路由表(一个列表)作为参数,传递给Application类的构造函数,创建了一个实例,然后再把这个实例传递给http_server。那么当客户端发起”get /”请求的时候,http server接收到这个请求,在理由表中匹配url pattern,最后交给MainHandler去处理。

路由表项

在tornado中,一个路由表项可以是一个元组(元组最终会转换成URLSpec对象)或者一个URLSpec对象。

class URLSpec:
  def __init__(self, pattern, handler, kwargs=None, name=None):

一个路由表项,最少含有两个元素pattern(一个正则表达式字符串)和handler(RequestHandler子类类型)。

值得一提的是还可以有两个可选元素,kwargsname

pattern

我们在浏览博客的时候,常常会看到这样的url,如下:

http://blog.csdn.net/wyx819/article/details/45652713

当然,在浏览器的地址栏输入这个url,会看到我的博客《tornaodo模板引擎原理》。我们完成可以想象,这个过程在csdn的博客http server中是如何进行的。csdn的httpserver解析这个url,拿到我的昵称”wyx819”和博客的postid”45652713”,然后交给handler,根据昵称和postid到数据库或者其他存储中取到我的《tornaodo模板引擎原理》这篇博客,经过render后,返回给客户端。

如果用tornado开发,假设web应用开发人员已经写好了,下面的handler。

def DisplayHandler(RequestHandler):
    def get(self, nickname, postid):
        entry = self.db.get("SELECT * FROM entries WHERE nickname = %s and postid = %d", slug, postid)
        if not entry: raise tornado.web.HTTPError(404)
        self.render("entry.html", entry=entry)

我们该如何写url路由内?
实际上所谓的url pattern在tornado中,是实实在在的正则表达式,因此需要一个正则表达式能够从http://blog.csdn.net/wyx819/article/details/45652713中提取出,”wyx819”和”45652713”。

在python标准库中,正则表达式主要由re模块处理。re模块的学习和使用,请参考这里

标准的正则表达式,本来提供分组(group)的语法,如”gr(a|e)y”,如果有子字符串匹配到元字符’(‘和’)’中的正则表达式,该子字符串会被作为一个孤立的group提取出来,在re模块中可以有Match.group方法,访问到该字符串。

在python的正则表达式re模块,进一步扩充了group语法,增加”named group”,如“(P?pattern)”,可以为括号中的正则表达式pattern命名(在尖括号里),最后通过Match.groupdict()方法返回一个字典,group name是该字典的key。

所以,我们可以根据python正则表达式的语法,写出如下的url pattern

application = tornado.web.Application([
        (r"/(P?<nickname>.*)/article/details/(P?<postid>.*)", MainHandler),
    ])

技术分享
在tornado框架中,m.groupdict()[‘nickname’]与m.groupdict()[‘postid’],会作为命名参数传递给DisplayHandler实例get方法。

handler(RequestHandler)

handler是路由表项中的第二个必要元素,如同MVC中的控制器,web应用开发者可以继承RequestHandler,实现RequestHandler中提供的接口方法(“get()”,“put()”等等。

kwargs(handler constructor’s arguments)

kwargs是路由表项的第三个可选元素,是一个字典,主要和RequestHandler中的initialize方法有关。initialize方法是RequestHandler函数中的钩子,RequestHandler子类,可以重写这个钩子函数:

class ProfileHandler(RequestHandler):
    def initialize(self, database):
        self.database = database

    def get(self, username):
        ...

app = Application([ (r‘/user/(.*)‘, ProfileHandler, dict(database=database)),])

kwargs字典中每一项,会作为RequestHandler子类的成员变量,将子类初始化。

题外话

如上面的例子,要使ProfileHandler类中的方法可以访问到database 实例,还有其他方法,其中一个是Application子类化。

class RequestHandler(object):
    def __init__(self, application, request, **kwargs):

从RequestHandler的构造函数中,我们可以看到它里面的方法是可以访问到Application实例的,因此我们子类Application,在其中添加自定义的成员变量和函数,也算是应用级别的一种数据,方法共享。

name(handler’s name)

name是路由表项中第四个可选元素,主要为一个Handler命名,以便在模板中,可以通过”{{ reverse_url(name)}}”获得实际的url,那么在模板中,就不必硬编码url地址(在前面博文模板引擎原理中提到)。

模板中使用的reverse_url函数调用的就是RequestHandler中的reverse_url函数,实际上最终调用的是Application中的reverse_url函数

class Application:
    def add_handlers(self, host_pattern, host_handlers):
        if not host_pattern.endswith("$"):
            host_pattern += "$"
        handlers = []
        if self.handlers and self.handlers[-1][0].pattern == ‘.*$‘:
            self.handlers.insert(-1, (re.compile(host_pattern), handlers))
        else:
            self.handlers.append((re.compile(host_pattern), handlers))

        for spec in host_handlers:
            if isinstance(spec, (tuple, list)):
                assert len(spec) in (2, 3, 4)
                spec = URLSpec(*spec)
            handlers.append(spec)
            if spec.name:
                if spec.name in self.named_handlers:
                    app_log.warning(
                        "Multiple handlers named %s; replacing previous value",
                        spec.name)
                self.named_handlers[spec.name] = spec

Application类在add_handlers方法中,如果发现一个URLSpec对象name成员不为None,那么把它加入字典named_handlers中,因此Application类是有能力根据name反过来获取实际的url。

url匹配流程源码分析

整个url匹配和分配handler的过程主要体现在_RequestDispatcher类的_find_handler方法和execute方法中。

def _find_handler(self):
        app = self.application
        handlers = app._get_host_handlers(self.request)
        if not handlers:
            self.handler_class = RedirectHandler
            self.handler_kwargs = dict(url="%s://%s/" % (self.request.protocol, app.default_host))
            return
        for spec in handlers:
            match = spec.regex.match(self.request.path)
            if match:
                self.handler_class = spec.handler_class
                self.handler_kwargs = spec.kwargs
                if spec.regex.groups:
                    if spec.regex.groupindex:
                        self.path_kwargs = dict(
                            (str(k), _unquote_or_none(v))
                            for (k, v) in match.groupdict().items())
                    else:
                        self.path_args = [_unquote_or_none(s)
                                          for s in match.groups()]
                return
        if app.settings.get(‘default_handler_class‘):
            self.handler_class = app.settings[‘default_handler_class‘]
            self.handler_kwargs = app.settings.get(
                ‘default_handler_args‘, {})
        else:
            self.handler_class = ErrorHandler
            self.handler_kwargs = dict(status_code=404)

从_find_handler方法中,我们可以看到,tornado按照顺序遍历路由表,拿request.path去匹配路由表中的url pattern。如果匹配上,将路由表项handler类(路由表第二个元素)保存到handler_class,将kwargs(路由表中的第三个元素)保存到handler_kwargs中,如果match对象中存在group,那么提取path参数,保存在path_kwargs或者path_args中。

如果遍历完路由表,都没有匹配上url pattern,看settings中有没配置”default_handler_class”。

如果没有”default_handler_class”,最后交给ErrorHandler处理,客户端会看到一个404的页面。

def execute(self):
        if not self.application.settings.get("compiled_template_cache", True):
            with RequestHandler._template_loader_lock:
                for loader in RequestHandler._template_loaders.values():
                    loader.reset()
        if not self.application.settings.get(‘static_hash_cache‘, True):
            StaticFileHandler.reset()

        self.handler = self.handler_class(self.application, self.request,
                                          **self.handler_kwargs)
        transforms = [t(self.request) for t in self.application.transforms]

        if self.stream_request_body:
            self.handler._prepared_future = Future()
        f = self.handler._execute(transforms, *self.path_args, **self.path_kwargs)
        f.add_done_callback(lambda f: f.exception())
        return self.handler._prepared_future

在execute方法中,我们看到以下代码:

f = self.handler._execute(transforms, *self.path_args, **self.path_kwargs)

前面_find_handler取到的path参数path_args和path_kwargs,最终交给RequestHander类的_execute方法去处理。
在RequestHandler中的_execute方法,我们可以看到以下代码:

result = method(*self.path_args, **self.path_kwargs)

拿前面csdn博客的例子来说,最后调用到DisplayHandler的get方法,并且得到nickname和postid这两个path参数。

重定向

关于重定向,一个非常典型的例子是当用户访问一些需要授权才能访问的页面的时候,这个时候网站的反应应该是重定向到登陆页面。而这一点在tornado中,是如何做的?

需要给授权页面对应的Handler中的http方法,加入authenticated函数装饰器。

def authenticated(method):
    @functools.wraps(method)
    def wrapper(self, *args, **kwargs):
        if not self.current_user:
            if self.request.method in ("GET", "HEAD"):
                url = self.get_login_url()
                if "?" not in url:
                    if urlparse.urlsplit(url).scheme:
                        # if login url is absolute, make next absolute too
                        next_url = self.request.full_url()
                    else:
                        next_url = self.request.uri
                    url += "?" + urlencode(dict(next=next_url))
                self.redirect(url)
                return
            raise HTTPError(403)
        return method(self, *args, **kwargs)
    return wrapper

如果发现用户没有登陆,会调用RequestHandler.redirect(url),这个要求用户得首先设置login_url。这个设计上,我觉得不应该直接设置url,而应该设置login_url对应的Handler的名字(name),因为url可能会经常改变,而Handler的名字不会经常改变,tornado中应该提供这样的一个函数,根据Handler的name重定向,当然首先调用reverse_url,再调用redirect方法就可以。

如果在路由表中,我们需要直接设置重定向,该怎么办?
tornado中也为我们处理好了,框架中内置了一个RedirectHandler

HTTP方法限制

在web应用开发过程,我们通常希望限制Handler只能处理某些http方法。
个人觉得应该在路由表中加入第五个元素:

restrict_method=["POST"]

当然这个在tornado中是只有支持,

class RequestHandler(object):
    SUPPORTED_METHODS = ("GET", "HEAD", "POST", "DELETE", "PATCH", "PUT",
                         "OPTIONS")

通过RequestHandler的SUPPORTED_METHODS列表,子类中可以重写。

class RequestHandler
    def _execute(self, transforms, *args, **kwargs):
        """Executes this request with the given output transforms."""
        self._transforms = transforms
        try:
            if self.request.method not in self.SUPPORTED_METHODS:
                raise HTTPError(405)
        ...

如果发现http请求的方法在Handler中不支持,Handler不再做处理,提早返回一个405页面(资源禁止访问)。

只不过当到RequestHandler._execute开始执行的时候,整个http协议header都已经解析完成了。个人觉得对支持方法的检测可以提早,因为http协议的方法是在请求行中。可以在解析完请求行后,就直接判断对应的Handler是否支持该方法,不必解析完整个header。

路由机制待改善地方

其实路由规则可以非常灵活,可以让我们diy,满足更加多的需求,比如特殊条件限制(限制某些网段的ip才可以访问某个url)。只不过tornado框架,它并不是作为一个非常完善的web开发框架,也许这个框架的重点就不在这里。作为学习的角度来说,我们也应该对此多做了解。路由规则方面可以参考这里,rubyonrails的路由规则。

tornado路由(Routing)

标签:python   tornado   web   框架   

原文地址:http://blog.csdn.net/wyx819/article/details/45936499

(0)
(0)
   
举报
评论 一句话评论(0
登录后才能评论!
© 2014 mamicode.com 版权所有  联系我们:gaon5@hotmail.com
迷上了代码!