您的位置:首页 > 其它

web.py浅析

2015-07-06 13:25 197 查看

web.py架构分析

我们可以将整个项目分层三层:应用层,中间层,网络层。其中,应用层负责处理应用逻辑,并暴露出用户app的一个wsgi callable;中间层提供应用层和网络层的对接服务,用户可以在这里选择使用不同的服务器模式来执行你的wsgi app,例如CGI、fastCGI、SCGI模式的服务器;网络层主要是实现一个webpy自带的基于多线程CGI模式的服务器或者调用CherryPy服务器,而fastCGI模式和SCGI模式的服务器由flup提供实现。

应用框架的主要逻辑在application.py文件。

用户对服务器进行请求之后,服务器将请求相关信息进行处理(主要是得到两个wsgi callable调用所需要的参数environ和start_response)并且传给wsgi callable进行调用,这个wsgi callable就是wsgifunc函数。

def wsgifunc(self, *middleware):
        """Returns a WSGI-compatible function for this application."""
        def peep(iterator):
            # 窥视就是取得一个值又chain起来
            """Peeps into an iterator by doing an iteration
            and returns an equivalent iterator.
            """
            # wsgi requires the headers first
            # so we need to do an iteration
            # and save the result for later

            # 生成器也能peep
            try:
                firstchunk = iterator.next()
            except StopIteration:
                firstchunk = ''

            return itertools.chain([firstchunk], iterator)

        def is_generator(x): return x and hasattr(x, 'next')

        def wsgi(env, start_resp):
            # threadLocal变量
            # clear threadlocal to avoid inteference of previous requests
            # # 在里面对web.ctx进行设定,而因为ctx继承threadlocal,所以对于每个线程都是独立的

            # 1、清理线程threadlocal变量,因为请求的很多内容都是threadlocal变量
            self._cleanup()

            # 使用 env 中的参数初始化 web.ctx 变量,这些变量涵盖了当前请求的信息,我们在应用中有可能会使用到
            # 2、将env变量转变为threadlocal变量
            self.load(env)
            try:
                # allow uppercase methods only
                if web.ctx.method.upper() != web.ctx.method:
                    raise web.nomethod()
                # 3、处理请求
                # processor和中间件不同

                # 这条是灵魂语句
                result = self.handle_with_processors()
                if is_generator(result):
                    # 如果本来就是generator,就硬生生拆成两个????
                    # 返回一个可迭代对象,则进行安全迭代处理(????)
                    result = peep(result)
                else:
                    # 返回其他值,则创建一个列表对象来存放。
                    result = [result]
            except web.HTTPError, e:
                result = [e.data]

            # Converts any given object to utf-8 encoded string
            # 4、将响应转化为utf-8的字符串
            result = web.safestr(iter(result))

            status, headers = web.ctx.status, web.ctx.headers

            # 5、根据WSGI的规范作如下两个事情
            # 5.1、调用 start_resp 函数。
            start_resp(status, headers)

            def cleanup():
                self._cleanup()
                yield '' # force this function to be a generator

            # 用itertools.chain来确定一种callback关系
            # 5.2将 result 结果转换成一个迭代器。
            return itertools.chain(result, cleanup())

        # 用middlerware用来对函数进行包装,这就是middleware的方式?
        # 6、添加中间件
        for m in middleware:
            wsgi = m(wsgi)

        return wsgi


这里的关键是handle_with_processors函数:

def handle_with_processors(self):
        def process(processors):
            try:
                if processors:
                    # 要先干掉处理器
                    p, processors = processors[0], processors[1:]
                    '''
                    例如有两个processor
                    一个
                    def addHello(handler):
                        return "hello" + handler()

                    另一个
                    def addShit(handler):
                        return handler() + "shit"

                    self.processors = [addHello,addShit]
                    那么第一轮返回addHello(process([addShit]))
                    第二轮返回addHello(addShit(process([])))
                    第三轮addHello(addShit(handle()))
                    '''
                    return p(lambda: process(processors))
                else:
                    # 真正处理请求
                    return self.handle()
            except web.HTTPError:
                raise
            except (KeyboardInterrupt, SystemExit):
                raise
            except:
                print >> web.debug, traceback.format_exc()
                raise self.internalerror()

        # processors must be applied in the resvere order. (??)
        return process(self.processors)


关键是handle函数:

def handle(self):
        # 进行路由功能,找到对应的类或者子应用
        fn, args = self._match(self.mapping, web.ctx.path)
        # 调用这个类或者传递请求到子应用
        return self._delegate(fn, self.fvars, args)


# key————————URL路由
    def _match(self, mapping, value):
        # path:handler_class
        for pat, what in mapping:
            # 这里的application只得是不加括号的,加了括号就是basestring了
            if isinstance(what, application):
                # 只要startswith what就交给子应用
                if value.startswith(pat):
                    # 交给子应用处理
                    f = lambda: self._delegate_sub_application(pat, what)
                    return f, None
                else:
                    continue
            elif isinstance(what, basestring):
                # ”如果是字符串“——————最普遍的情况
                # t, m = re_subm('g(oo+)fball', r'f\\1lish', 'goooooofball')
                # 用value和'^' + pat + '$'配,如果有匹配的就放到what那里变成t
                # m是match————匹配的部分

                # 例如["/index","IndexHandler"]
                # web.ctx.path为/index
                # 那么正则表达式就是"^/index$",刚好可以匹配,没有替换就等于没有参数
                # 用过web.py都知道url里面匹配到的东西会作为参数传递

                # 又例如["/ak/([0-9])+","AkHandler"]
                # web.ctx.path为/ak/23
                # class AkHandler():
                #   ...
                #   def GET(self,number):
                #       ...
                # 那么此时匹配到23,并且放到了result里面去,然后下面返回的时候就逐个从.groups()里面取出了,传出去作为参数
                what, result = utils.re_subm('^' + pat + '$', what, value)
            else:
                # 如果what不是子app又不是字符串,比如直接指定一个类对象作为处理对象。
                # 直接传回(类对象,参数)
                result = utils.re_compile('^' + pat + '$').match(value)

            if result: # it's a match
                return what, [x for x in result.groups()]
        return None, None


# key——————调用方法对应的函数
    def _delegate(self, f, fvars, args=[]):
        # 真正地调用方法
        def handle_class(cls):
            meth = web.ctx.method
            # 如果没有定义HEAD方法,就转为调用GET方法
            if meth == 'HEAD' and not hasattr(cls, meth):
                meth = 'GET'
            if not hasattr(cls, meth):
                raise web.nomethod(cls)
            # 先得到那个方法,然后传入args调用
            tocall = getattr(cls(), meth)
            # 返回一个调用
            return tocall(*args)

        # 判断是不是一个累
        def is_class(o): return isinstance(o, (types.ClassType, type))

        if f is None:
            raise web.notfound()
        # 如果是application就用那个application实例(instance)去handler,就是说多个app同时运行?
        elif isinstance(f, application):
            return f.handle_with_processors()
        # 如果是个类,用那个类去处理
        elif is_class(f):
            return handle_class(f)
        # 如果是字符串
        elif isinstance(f, basestring):
            if f.startswith('redirect '):
                # 支持“redirect url”这种语法?
                url = f.split(' ', 1)[1]
                if web.ctx.method == "GET":
                    x = web.ctx.env.get('QUERY_STRING', '')
                    if x:
                        url += '?' + x
                raise web.redirect(url)
            elif '.' in f:
                mod, cls = f.rsplit('.', 1)
                mod = __import__(mod, None, None, [''])
                cls = getattr(mod, cls)
            else:
                cls = fvars[f]
            return handle_class(cls)
        elif hasattr(f, '__call__'):
            return f()
        else:
            return web.notfound()


其中对于应用和子应用的问题,设计得非常精巧:使用一个threadlocal的变量建立一个app_stack列表,当调用子应用的时候,就将当前的context压入app_stack,在子应用调用完之后从app_stack重新取出原来的context。

因为整个请求使用线程来处理,所以webpy把很多重要的变量例如context和environ这样的变量做成threadlocal变量了。

按照官方文档中的例子,我们对app的调用可以是这样子的:

app = application(environ,start_response)
app.run()


run函数如下:

def run(self, *middleware):
        """
        Starts handling requests. If called in a CGI or FastCGI context, it will follow
        that protocol. If called from the command line, it will start an HTTP
        server on the port named in the first command line argument, or, if there
        is no argument, on port 8080.

        `middleware` is a list of WSGI middleware which is applied to the resulting WSGI
        function.
        """
        # 使用内置的flup服务器运行
        # app.run() 的调用是初始化各种WCGI接口,并启动一个内置的HTTP服务器和这些接口对接
        return wsgi.runwsgi(self.wsgifunc(*middleware))


wsgi.runwsgi函数如下:

这个没自己指定,反正给我run起来

def runwsgi(func):
    """
    Runs a WSGI-compatible `func` using FCGI, SCGI, or a simple web server,
    as appropriate based on context and `sys.argv`.
    """

    if os.environ.has_key('SERVER_SOFTWARE'): # cgi
        os.environ['FCGI_FORCE_CGI'] = 'Y'

    if (os.environ.has_key('PHP_FCGI_CHILDREN') #lighttpd fastcgi
      or os.environ.has_key('SERVER_SOFTWARE')):
        return runfcgi(func, None)

    if 'fcgi' in sys.argv or 'fastcgi' in sys.argv:
        args = sys.argv[1:]
        if 'fastcgi' in args: args.remove('fastcgi')
        elif 'fcgi' in args: args.remove('fcgi')
        if args:
            return runfcgi(func, validaddr(args[0]))
        else:
            return runfcgi(func, None)

    if 'scgi' in sys.argv:
        args = sys.argv[1:]
        args.remove('scgi')
        if args:
            return runscgi(func, validaddr(args[0]))
        else:
            return runscgi(func)

    server_addr = validip(listget(sys.argv, 1, ''))
    if os.environ.has_key('PORT'): # e.g. Heroku
        server_addr = ('0.0.0.0', intget(os.environ['PORT']))

    # 网络层,运行方式为cgi形式的网络层实现,它是基于Python已有的SimpleHTTPServer和BaseHTTPServer实现的。
    # 当请求/static/时直接返回文件内容,其它请求则调用指向application中wsgifunc()的handler。
    return httpserver.runsimple(func, server_addr)


其中runsimple函数使用了CherryPy的基于线程池的wsgi服务器,而runfcgi和runscgi使用了flup的实现(flup: 一个用python写的web server,也就是cgi中所谓的Server/Gateway,它负责接受apache/lighttpd转发的请求,并调用你写的程序 (application),并将application处理的结果返回到apache/lighttpd)

另外一个比较有意思的是webpy的模板系统,它在template.py中实现

这个模板系统使用了很多编译原理的知识,包括产生式、文法定义、终结符和非终结符、语法分析树、lookahead等等编译原理知识。

下面是源码中对这个模板系统的一个介绍:

Template design:

Template string is split into tokens and the tokens are combined into nodes.
Parse tree is a nodelist. TextNode and ExpressionNode are simple nodes and
for-loop, if-loop etc are block nodes, which contain multiple child nodes.

模板的字符串 被分成许多token,然后token被包装成节点node,解析树就是一个node列表。TextNode和ExpressionNode是简单节点(终结符)
然后for循环和if循环是块节点————它可以包含很多个子节点(非终结符)

Each node can emit some python string. python string emitted by the
root node is validated for safeeval and executed using python in the given environment.

每个节点可以发射出python字符串,被根节点发射出来的python字符串被用safeeval验证然后使用python运行

Enough care is taken to make sure the generated code and the template has line to line match,
so that the error messages can point to exact line number in template. (It doesn't work in some cases still.)

每段生成的代码会被小心的处理以保证和模板代码行行对应,以使得错误信息可以指向确切的行数

Grammar:(产生式)

    template -> defwith sections
    defwith -> '$def with (' arguments ')' | ''
    sections -> section*
    section -> block | assignment | line

    assignment -> '$ ' 
    line -> (text|expr)*
    text ->


另外,webpy的内置HTTP服务器使用的是CherryPy的服务器,那是一个基于线程池的服务器

代码可以在wsgiserver/init.py中找到

其中的HTTPServer、HTTPRequest、HTTPConnection、WSGIGateway都直接继承自object,十分值得参考

其中调用顺序是:

CherryPyWSGIServer(HTTPServer).start()

HTTPServer.start()

bind socket

start ThreadPool

listen on port(tick())

HTTPServer.tick()

accept() new socket

create HTTPConnection

put the conn into the Queue

WorkerThread方面:

WorkerThread(threading.Thread)

get HTTPConnection from the Queue

HTTPConnection.communicate()

create a HTTPRequest

parse the request

give the response(respond())

HTTPRequest.respond()

HTTPServer.gateway.respond()

HTTPRequest.server.wsgi_app(env,start_response)

WSGIGateway.write()

HTTPRequest.write(…..)

HTTPRequest is responsible for both the handling of request and response

另外,值得一提的是另外一个函数,它作为wsgi的中间件:

class WSGIPathInfoDispatcher(object):
    """A WSGI dispatcher for dispatch based on the PATH_INFO.

    apps: a dict or list of (path_prefix, app) pairs.
    """

    def __init__(self, apps):
        try:
            apps = apps.items()
        except AttributeError:
            pass

        # Sort the apps by len(path), descending
        apps.sort(cmp=lambda x,y: cmp(len(x[0]), len(y[0])))
        apps.reverse()

        # The path_prefix strings must start, but not end, with a slash.
        # Use "" instead of "/".
        self.apps = [(p.rstrip("/"), a) for p, a in apps]

    def __call__(self, environ, start_response):
        path = environ["PATH_INFO"] or "/"
        for p, app in self.apps:
            # The apps list should be sorted by length, descending.
            if path.startswith(p + "/") or path == p:
                environ = environ.copy()
                environ["SCRIPT_NAME"] = environ["SCRIPT_NAME"] + p
                environ["PATH_INFO"] = path[len(p):]
                return app(environ, start_response)

        start_response('404 Not Found', [('Content-Type', 'text/plain'),
                                         ('Content-Length', '0')])
        return ['']


它的使用方式为:

d = WSGIPathInfoDispatcher({'/': my_crazy_app, '/blog': my_blog_app})
    server = wsgiserver.CherryPyWSGIServer(('0.0.0.0', 80), d)


WSGI将 web 组件分为三类: web服务器,web中间件,web应用程序, wsgi基本处理模式为 : WSGI Server/gateway -> (WSGI Middleware)* -> WSGI Application 。

.

wsgiserver/__init__.py这里的CherryPyWSGIServer相当于实现了server部分,而WSGIGateway、WSGIGateway_10、WSGIGateway_u0则实现了gateway部分,而WSGIPathInfoDispatcher函数则实现了Middleware部分(实现了URL Routing),wsgi application部分则由application.py来实现

所以说,webpy其实包含了对wsgi协议的一个非常完整的实现

下面是一堆对这三部分的描述:

WSGI Server/gateway。wsgi server可以理解为一个符合wsgi规范的web server,接收request请求,封装一系列环境变量,按照wsgi规范调用注册的wsgi app,最后将response返回给客户端。

wsgi application就是一个普通的callable对象,当有请求到来时,wsgi server会调用这个wsgi app。这个对象接收两个参数,通常为environ,start_response。environ就像前面介绍的,可以理解为环境变量,跟一次请求相关的所有信息都保存在了这个环境变量中,包括服务器信息,客户端信息,请求信息。start_response是一个callback函数,wsgi application通过调用start_response,将response headers/status 返回给wsgi server。此外这个wsgi app会return 一个iterator对象 ,这个iterator就是response body。

WSGI MiddleWare.有些功能可能介于服务器程序和应用程序之间,例如,服务器拿到了客户端请求的URL, 不同的URL需要交由不同的函数处理,这个功能叫做 URL Routing,这个功能就可以放在二者中间实现,这个中间层就是 middleware。middleware对服务器程序和应用是透明的,也就是说,服务器程序以为它就是应用程序,而应用程序以为它就是服务器。这就告诉我们,middleware需要把自己伪装成一个服务器,接受应用程序,调用它,同时middleware还需要把自己伪装成一个应用程序,传给服务器程序。其实无论是服务器程序,middleware 还是应用程序,都在服务端,为客户端提供服务,之所以把他们抽象成不同层,就是为了控制复杂度,使得每一次都不太复杂,各司其职。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: