tornado高效开发必备之源码详解

前言:本博文重在tornado源码剖析,相信读者读完此文能够更加深入的了解tornado的运行机制,从而更加高效的使用tornado框架。

本文参考武sir博客地址:http://www.cnblogs.com/wupeiqi/tag/Tornado/

初识tornado

首先我们从经典的hello world 案例入手:

import tornado.ioloop
import tornado.web
 
class MainHandler(tornado.web.RequestHandler):
    def get(self):
        self.write("Hello, world")
 
application = tornado.web.Application([
    (r"/index", MainHandler),
])
 
if __name__ == "__main__":
    application.listen(8888)
    tornado.ioloop.IOLoop.instance().start()

运行该脚本,依次执行:

  • 创建一个Application对象,并把一个正则表达式'/'和类名MainHandler传入构造函数:tornado.web.Application(...)  
  • 执行Application对象的listen(...)方法,即:application.listen(8888)
  • 执行IOLoop类的类的 start() 方法,即:tornado.ioloop.IOLoop.instance().start()

1.首先我们来分析如下代码:

application = tornado.web.Application([
    (r"/index", MainHandler),
])

  源码截图:

  由上图Application类的源码我们可以看出,需要传入的第一个参数是handlers,即上述代码可以进行如下表示:

application = tornado.web.Application(handlers=[
    (r"/index", MainHandler),
])

  这里的参数handlers非常重要,值得我们更加深入的研究。它应该是一个元组组成的列表,其中每个元组的第一个元素是一个用于匹配的正则表达式,第二个元素是一个RequestHanlder类。在hello.py中,我们只指定了一个正则表达式-RequestHanlder对,但你可以按你的需要指定任意多个。

2.Application类的深入:
源码截图:

这是源码中关于静态文件路径的描述,从上到下的意思依次为:

  • 如果用户在settings有配置名为“static_path”的文件路径(这就要求我们如果需要配置静态文件路径,则key值必须是“static_path”)
  • 从源码可看出,这是根据字典的方式进行取值,因此可断定settings是字典格式
  • ,这是配置静态文件路径前缀,便于tronado在前端的静态文件路径中找到静态文件,即告诉tronado,我是静态文件,按照静态文件路径查询即可,这里,静态文件前缀也可以理解为静态文件标识。

     

3.application.listen(8888):

 application.listen(8888)

 即Application类的listen方法:

源码截图:

从上述源码可看出listen方法接收port端口,address即ip地址,默认为空。然后实例化HTTPServer对象,执行listen方法:

HTTPServer类listen方法源码截图:

上述listen方法绑定了ip和端口,并开启socket。

bind方法源码截图:

 

由上述源码可看出,bind方法内部创建socket对象,调用socket对象绑定ip和端口,并进行监听。

4.tornado.ioloop.IOLoop.instance().start():

tornado.ioloop.IOLoop.instance().start()

  这是tronado的ioloop.py文件中的IOLoop类:

instance方法源码截图:

这里使用了类方法,即可以通过类名直接访问。我们需要关注的是最后面的代码:

if not hasattr(cls, "_instance"):
            cls._instance = cls()
        return cls._instance

 注:这里使用了单例模式,因为我们不需要为每一次连接IO都创建一个对象,换句话说,每次连接IO只需要是同一个对象即可

start方法:

class IOLoop(object):
    def add_handler(self, fd, handler, events):
        #HttpServer的Start方法中会调用该方法
        self._handlers[fd] = stack_context.wrap(handler)
        self._impl.register(fd, events | self.ERROR)
         
    def start(self):
        while True:
            poll_timeout = 0.2
            try:
                #epoll中轮询
                event_pairs = self._impl.poll(poll_timeout)
            except Exception, e:
                #省略其他
            #如果有读可用信息,则把该socket对象句柄和Event Code序列添加到self._events中
            self._events.update(event_pairs)
            #遍历self._events,处理每个请求
            while self._events:
                fd, events = self._events.popitem()
                try:
                    #以socket为句柄为key,取出self._handlers中的stack_context.wrap(handler),并执行
                    #stack_context.wrap(handler)包装了HTTPServer类的_handle_events函数的一个函数
                    #是在上一步中执行add_handler方法时候,添加到self._handlers中的数据。
                    self._handlers[fd](fd, events)
                except:
                    #省略其他

  由上述源码中while Ture:可以看出当执行了start方法后程序会进入死循环,不断检测是否有用户发送请求过来,如果有请求到达,则执行封装了HttpServer类的_handle_events方法和相关上下文的stack_context.wrap(handler)(其实就是执行HttpServer类的_handle_events方法),详细见下篇博文,简要代码如下:

class HTTPServer(object):
    def _handle_events(self, fd, events):
        while True:
            try:
                connection, address = self._socket.accept()
            except socket.error, e:
                if e.args[0] in (errno.EWOULDBLOCK, errno.EAGAIN):
                    return
                raise
            if self.ssl_options is not None:
                assert ssl, "Python 2.6+ and OpenSSL required for SSL"
                try:
                    connection = ssl.wrap_socket(connection,
                                                 server_side=True,
                                                 do_handshake_on_connect=False,
                                                 **self.ssl_options)
                except ssl.SSLError, err:
                    if err.args[0] == ssl.SSL_ERROR_EOF:
                        return connection.close()
                    else:
                        raise
                except socket.error, err:
                    if err.args[0] == errno.ECONNABORTED:
                        return connection.close()
                    else:
                        raise
            try:
                if self.ssl_options is not None:
                    stream = iostream.SSLIOStream(connection, io_loop=self.io_loop)
                else:
                    stream = iostream.IOStream(connection, io_loop=self.io_loop)
                HTTPConnection(stream, address, self.request_callback,
                               self.no_keep_alive, self.xheaders)
            except:
                logging.error("Error in connection callback", exc_info=True)

5.tornado.web.RequestHandler

这是所有业务处理handler需要继承的父类,接下来,介绍一些RequestHandler类中常用的一些方法:

 

  •  initialize:

从源码中可以看出initialize函数会在RequestHandler类初始化的时候执行,但是源码中initialize函数并没有做任何事情,这其实是tornado为我们预留的修改源码的地方,这就允许程序在执行所有的handler前首先执行我们在initialize中定义的方法。

  • write

源码截图:

write方法是后端向前端页面直接展示的方法。从上述源码中可以看出,write方法接收字典和字符串类型的参数,如果用户传来的数据是字典类型,源码中会自动用json对字典进行序列化,最终序列化成字符串。

self._write_buffer是源码中定义的一个临时存放需要输出的字符串的地方,是列表格式。

  • 需要write的内容添加到 self._write_buffer后,系统会执行flush方法:

flush方法源码截图:

 

 由上述源码可以看出:flush方法会self._write_buffer列表中的所有元素拼接成字符串,并赋值给chunk,然后清空self._write_buffer列表,然后设置请求头,最终调用request.write方法在前端页面显示。

  • render方法:

源码截图:

由上述源码可看出render方法是根据参数渲染模板,下面我们来介绍具体源码中是如何渲染的:

js和css部分的源码截图:

由上述源码可看出,静态文件(以JavaScript为例,css是类似的)的渲染流程是:

首先通过module.embedded_javascript() 获取需要插入JavaScript字符串,添加到js_embed 列表中;

进而通过module.javascript_files()获取已有的列表格式的JavaScript files,最终将它加入js_files.

下面对js_embed和js_files做进一步介绍:

js_embed源码截图:

上图源码即生成script标签,这是一些我们自己定义的一些JavaScript代码;最终是通过字符串拼接方式插入到整个html中。

js_files源码截图:

 

 

上图源码即生成script标签,这是一些需要引入的JavaScript代码块;最终是通过字符串拼接方式插入到整个html中。需要注意的是:其中静态路径是调用self.static_url(path)实现的。

static_url方法源码截图:

由上述代码可看出:源码首先会判断用户有没有设置静态路径的前缀,然后将静态路径与相对路径进行拼接成绝对路径,接下来按照绝对路径打开文件,并对文件内容(f.read())做md5加密,最终将根目录+静态路径前缀+相对路径拼接在前端html中展示。

 

render_string方法:

这是render方法中最重要的一个子方法,它负责去处理Html模板并返回最终结果:

详细流程:

 

    1. 创建Loader对象,并执行load方法
          -- 通过open函数打开html文件并读取内容,并将内容作为参数又创建一个 Template 对象
          -- 当执行Template的 __init__ 方法时,根据模板语言的标签 {{}}、{%%}等分割并html文件,最后生成一个字符串表示的函数
    2. 获取所有要嵌入到html模板中的变量,包括:用户返回和框架默认
    3. 执行Template对象的generate方法
          -- 编译字符串表示的函数,并将用户定义的值和框架默认的值作为全局变量
          -- 执行被编译的函数获取被嵌套了数据的内容,然后将内容返回(用于响应给请求客户端)

 

源码注释:

class RequestHandler(object):
 
    def render_string(self, template_name, **kwargs):
         
        #获取配置文件中指定的模板文件夹路径,即:template_path = 'views'
        template_path = self.get_template_path()
 
        #如果没有配置模板文件的路径,则默认去启动程序所在的目录去找
        if not template_path:
            frame = sys._getframe(0)
            web_file = frame.f_code.co_filename
            while frame.f_code.co_filename == web_file:
                frame = frame.f_back
            template_path = os.path.dirname(frame.f_code.co_filename)
        if not getattr(RequestHandler, "_templates", None):
            RequestHandler._templates = {}
         
        #创建Loader对象,第一次创建后,会将该值保存在RequestHandler的静态字段_template_loaders中
        if template_path not in RequestHandler._templates:
            loader = self.application.settings.get("template_loader") or\
              template.Loader(template_path)
            RequestHandler._templates[template_path] = loader
 
        #执行Loader对象的load方法,该方法内部执行执行Loader的_create_template方法
        #在_create_template方法内部使用open方法会打开html文件并读取html的内容,然后将其作为参数来创建一个Template对象
        #Template的构造方法被执行时,内部解析html文件的内容,并根据内部的 {{}} {%%}标签对内容进行分割,最后生成一个字符串类表示的函数并保存在self.code字段中
        t = RequestHandler._templates[template_path].load(template_name)
         
        #获取所有要嵌入到html中的值和框架默认提供的值
        args = dict(
            handler=self,
            request=self.request,
            current_user=self.current_user,
            locale=self.locale,
            _=self.locale.translate,
            static_url=self.static_url,
            xsrf_form_html=self.xsrf_form_html,
            reverse_url=self.application.reverse_url
        )
        args.update(self.ui)
        args.update(kwargs)
 
        #执行Template的generate方法,编译字符串表示的函数并将namespace中的所有key,value设置成全局变量,然后执行该函数。从而将值嵌套进html并返回。
        return t.generate(**args) 
class Loader(object):
    """A template loader that loads from a single root directory.

    You must use a template loader to use template constructs like
    {% extends %} and {% include %}. Loader caches all templates after
    they are loaded the first time.
    """
    def __init__(self, root_directory):
        self.root = os.path.abspath(root_directory)
        self.templates = {}

Loader.__init__
Loader.__init__
class Loader(object):
    def load(self, name, parent_path=None):
        name = self.resolve_path(name, parent_path=parent_path)
        if name not in self.templates:
            path = os.path.join(self.root, name)
            f = open(path, "r")
            #读取html文件的内容
            #创建Template对象
            #name是文件名
            self.templates[name] = Template(f.read(), name=name, loader=self)
            f.close()
        return self.templates[name]

Loader.load
Loader.load
class Template(object):

    def __init__(self, template_string, name="<string>", loader=None,compress_whitespace=None):
        # template_string是Html文件的内容

        self.name = name
        if compress_whitespace is None:
            compress_whitespace = name.endswith(".html") or name.endswith(".js")
        
        #将内容封装到_TemplateReader对象中,用于之后根据模板语言的标签分割html文件
        reader = _TemplateReader(name, template_string)
        
        #分割html文件成为一个一个的对象
        #执行_parse方法,将html文件分割成_ChunkList对象
        self.file = _File(_parse(reader))
        
        #将html内容格式化成字符串表示的函数
        self.code = self._generate_python(loader, compress_whitespace)
        
        try:
            #将字符串表示的函数编译成函数
            self.compiled = compile(self.code, self.name, "exec")
            
        except:
            formatted_code = _format_code(self.code).rstrip()
            logging.error("%s code:\n%s", self.name, formatted_code)
            raise

Template.__init__
Template.__init__
class Template(object):
    def generate(self, **kwargs):
        """Generate this template with the given arguments."""
        namespace = {
            "escape": escape.xhtml_escape,
            "xhtml_escape": escape.xhtml_escape,
            "url_escape": escape.url_escape,
            "json_encode": escape.json_encode,
            "squeeze": escape.squeeze,
            "linkify": escape.linkify,
            "datetime": datetime,
        }

        #创建变量环境并执行函数,详细Demo见上一篇博文
        namespace.update(kwargs)
        exec self.compiled in namespace
        execute = namespace["_execute"]

        try:
            #执行编译好的字符串格式的函数,获取嵌套了值的html文件
            return execute()
        except:
            formatted_code = _format_code(self.code).rstrip()
            logging.error("%s code:\n%s", self.name, formatted_code)
            raise

Template.generate
Template.generate

示例html:

源码模板语言处理部分的截图:

结束语

  本博文从tornado  url正则匹配、路由与映射、底层socket实现、端口监听、多请求并发、Handler类方法的源码剖析,便于tronado web开发人员能够更深入的理解tronado的运行机制,从而更高效的从事tronado web开发。

如果您觉得本文对您有参考价值,欢迎帮博主点击文章下方的推荐,谢谢!

 


posted @ 2016-09-01 09:10  wangheng1409  阅读(3868)  评论(22编辑  收藏  举报