tornado源码分析
初识tornado
首先从经典的helloword案例入手
1 import tornado.ioloop 2 import tornado.web 3 4 class MainHandler(tornado.web.RequestHandler): 5 def get(self): 6 self.write("Hello, world") 7 8 application = tornado.web.Application([ 9 (r"/index", MainHandler), 10 ]) 11 12 if __name__ == "__main__": 13 application.listen(8888) 14 tornado.ioloop.IOLoop.instance().start()
运行该脚本,依次执行: 创建一个Application对象,并把一个正则表达式'/'和类名MainHandler传入构造函数:tornado.web.Application(...) 执行Application对象的listen(...)方法,即:application.listen(8888) 执行IOLoop类的类的 start() 方法,即:tornado.ioloop.IOLoop.instance().start()
1、分析如下代码
1 application = tornado.web.Application([
2 (r"/index", MainHandler),
3 ])
源码截图:
由上图Application类的源码我们可以看出,需要传入的第一个参数是handlers,即上述代码可以进行如下表示:
application = tornado.web.Application(handlers=[ (r"/index", MainHandler), ])
这里的参数handlers非常重要,值得我们更加深入的研究。它应该是一个元组组成的列表,其中每个元组的第一个元素是一个用于匹配的正则表达式,第二个元素是一个RequestHanlder类。在hello.py中,我们只指定了一个正则表达式-RequestHanlder对,但你可以按你的需要指定任意多个。
<span style="color: #3366ff;">2.Application类的深入:</span><br>源码截图:
这是源码中关于静态文件路径的描述,从上到下的意思依次为:
- 如果用户在settings有配置名为“static_path”的文件路径(这就要求我们如果需要配置静态文件路径,则key值必须是“static_path”)
- 从源码可看出,这是根据字典的方式进行取值,因此可断定settings是字典格式
- ,这是配置静态文件路径前缀,便于tronado在前端的静态文件路径中找到静态文件,即告诉tronado,我是静态文件,按照静态文件路径查询即可,这里,静态文件前缀也可以理解为静态文件标识。
application.listen(8888):
pplication.listen(8888)
即Application类的listen方法:
源码截图:
由上述源码可看出,bind方法内部创建socket对象,调用socket对象绑定ip和端口,并进行监听。
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方法:
1 class IOLoop(object): 2 def add_handler(self, fd, handler, events): 3 #HttpServer的Start方法中会调用该方法 4 self._handlers[fd] = stack_context.wrap(handler) 5 self._impl.register(fd, events | self.ERROR) 6 7 def start(self): 8 while True: 9 poll_timeout = 0.2 10 try: 11 #epoll中轮询 12 event_pairs = self._impl.poll(poll_timeout) 13 except Exception, e: 14 #省略其他 15 #如果有读可用信息,则把该socket对象句柄和Event Code序列添加到self._events中 16 self._events.update(event_pairs) 17 #遍历self._events,处理每个请求 18 while self._events: 19 fd, events = self._events.popitem() 20 try: 21 #以socket为句柄为key,取出self._handlers中的stack_context.wrap(handler),并执行 22 #stack_context.wrap(handler)包装了HTTPServer类的_handle_events函数的一个函数 23 #是在上一步中执行add_handler方法时候,添加到self._handlers中的数据。 24 self._handlers[fd](fd, events) 25 except: 26 #省略其他
由上述源码中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)
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模板并返回最终结果:
详细流程:
- 创建Loader对象,并执行load方法
-- 通过open函数打开html文件并读取内容,并将内容作为参数又创建一个 Template 对象
-- 当执行Template的 __init__ 方法时,根据模板语言的标签 {{}}、{%%}等分割并html文件,最后生成一个字符串表示的函数 - 获取所有要嵌入到html模板中的变量,包括:用户返回和框架默认
- 执行Template对象的generate方法
-- 编译字符串表示的函数,并将用户定义的值和框架默认的值作为全局变量
-- 执行被编译的函数获取被嵌套了数据的内容,然后将内容返回(用于响应给请求客户端)
源码注释:
1 class RequestHandler(object): 2 3 def render_string(self, template_name, **kwargs): 4 5 #获取配置文件中指定的模板文件夹路径,即:template_path = 'views' 6 template_path = self.get_template_path() 7 8 #如果没有配置模板文件的路径,则默认去启动程序所在的目录去找 9 if not template_path: 10 frame = sys._getframe(0) 11 web_file = frame.f_code.co_filename 12 while frame.f_code.co_filename == web_file: 13 frame = frame.f_back 14 template_path = os.path.dirname(frame.f_code.co_filename) 15 if not getattr(RequestHandler, "_templates", None): 16 RequestHandler._templates = {} 17 18 #创建Loader对象,第一次创建后,会将该值保存在RequestHandler的静态字段_template_loaders中 19 if template_path not in RequestHandler._templates: 20 loader = self.application.settings.get("template_loader") or\ 21 template.Loader(template_path) 22 RequestHandler._templates[template_path] = loader 23 24 #执行Loader对象的load方法,该方法内部执行执行Loader的_create_template方法 25 #在_create_template方法内部使用open方法会打开html文件并读取html的内容,然后将其作为参数来创建一个Template对象 26 #Template的构造方法被执行时,内部解析html文件的内容,并根据内部的 {{}} {%%}标签对内容进行分割,最后生成一个字符串类表示的
函数并保存在self.code字段中 27 t = RequestHandler._templates[template_path].load(template_name) 28 29 #获取所有要嵌入到html中的值和框架默认提供的值 30 args = dict( 31 handler=self, 32 request=self.request, 33 current_user=self.current_user, 34 locale=self.locale, 35 _=self.locale.translate, 36 static_url=self.static_url, 37 xsrf_form_html=self.xsrf_form_html, 38 reverse_url=self.application.reverse_url 39 ) 40 args.update(self.ui) 41 args.update(kwargs) 42 43 #执行Template的generate方法,编译字符串表示的函数并将namespace中的所有key,value设置成全局变量,然后执行该函数。从而
将值嵌套进html并返回。 44 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__
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
lass 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__
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
示例html:
源码模板语言处理部分的截图:
结束语
本博文从tornado url正则匹配、路由与映射、底层socket实现、端口监听、多请求并发、Handler类方法的源码剖析,便于深入的理解tronado的运行机制,更高效的进行tronado开发。