前言:本博文重在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开发。

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

												

tornado高效开发必备之源码详解的更多相关文章

  1. Activiti架构分析及源码详解

    目录 Activiti架构分析及源码详解 引言 一.Activiti设计解析-架构&领域模型 1.1 架构 1.2 领域模型 二.Activiti设计解析-PVM执行树 2.1 核心理念 2. ...

  2. 源码详解系列(六) ------ 全面讲解druid的使用和源码

    简介 druid是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,druid还扩展 ...

  3. 源码详解系列(七) ------ 全面讲解logback的使用和源码

    什么是logback logback 用于日志记录,可以将日志输出到控制台.文件.数据库和邮件等,相比其它所有的日志系统,logback 更快并且更小,包含了许多独特并且有用的特性. logback ...

  4. 源码详解系列(八) ------ 全面讲解HikariCP的使用和源码

    简介 HikariCP 是用于创建和管理连接,利用"池"的方式复用连接减少资源开销,和其他数据源一样,也具有连接数控制.连接可靠性测试.连接泄露控制.缓存语句等功能,另外,和 dr ...

  5. Mybatis源码详解系列(四)--你不知道的Mybatis用法和细节

    简介 这是 Mybatis 系列博客的第四篇,我本来打算详细讲解 mybatis 的配置.映射器.动态 sql 等,但Mybatis官方中文文档对这部分内容的介绍已经足够详细了,有需要的可以直接参考. ...

  6. [转]【视觉 SLAM-2】 视觉SLAM- ORB 源码详解 2

    转载地址:https://blog.csdn.net/kyjl888/article/details/72942209 1 ORB-SLAM2源码详解 by 吴博 2 https://github.c ...

  7. RocketMQ源码详解 | Broker篇 · 其五:高可用之主从架构

    概述 对于一个消息中间件来讲,高可用功能是极其重要的,RocketMQ 当然也具有其对应的高可用方案. 在 RocketMQ 中,有主从架构和 Dledger 两种高可用方案: 第一种通过主 Brok ...

  8. Spark Streaming揭秘 Day25 StreamingContext和JobScheduler启动源码详解

    Spark Streaming揭秘 Day25 StreamingContext和JobScheduler启动源码详解 今天主要理一下StreamingContext的启动过程,其中最为重要的就是Jo ...

  9. spring事务详解(三)源码详解

    系列目录 spring事务详解(一)初探事务 spring事务详解(二)简单样例 spring事务详解(三)源码详解 spring事务详解(四)测试验证 spring事务详解(五)总结提高 一.引子 ...

随机推荐

  1. linux tomcat 启动

    在tomcat bin目录下启动时报权限不够 在bin目录下输入:chmod u+x *.sh 可解决 查看tomcat 是否关闭 ps -ef|grep java 例如显示如下 说明还没关闭root ...

  2. python collections模块

    collections模块基本介绍 collections在通用的容器dict,list,set和tuple之上提供了几个可选的数据类型 namedtuple() factory function f ...

  3. 基于linux(centos)的svn环境搭建

    1. 安装svn yum intall subversion 2. 查看安装位置 rpm -ql subversion 3. 检验svn是否安装成功,查看帮助 svn --help , 看到下图表示成 ...

  4. MongoDB【第三篇】MongoDB基本操作

    MongoDB的基本操作包括文档的创建.删除.和更新 文档插入 1.插入 #查看当前都有哪些数据库 > show dbs; local 0.000GB tim 0.000GB #使用 tim数据 ...

  5. mybatis中#{}与${}的差别(如何防止sql注入)

    默认情况下,使用#{}语法,MyBatis会产生PreparedStatement语句中,并且安全的设置PreparedStatement参数,这个过程中MyBatis会进行必要的安全检查和转义. # ...

  6. mysql获取一个表中的下一个自增(id)值的方法

    SELECT Auto_increment FROM information_schema.`TABLES` WHERE Table_Schema='数据库名' AND table_name = '表 ...

  7. 一些关于angularJS的自己学习和开发过程中遇到的问题及解决办法

    这篇文章也许会不定时更新,主要记录这段时间内自己遇到的angularjs学习开发的一些问题的解决办法.本文以摘抄为主,主要目的还是将自己遇到的困惑在各个地方查到的解决办法的汇总,给自己留个备忘吧. 1 ...

  8. 利用Levenshtein Distance (编辑距离)实现文档相似度计算

    1.首先将word文档解压缩为zip /** * 修改后缀名 */ public static String reName(String path){ File file=new File(path) ...

  9. MFC双缓存技术代码

    屏蔽背景刷新,在View中添加对WM_ERASEBKGND的响应,直接返回TRUE: BOOL CTEMV1View::OnEraseBkgnd(CDC* pDC) { // TODO: 在此添加消息 ...

  10. 基于.net搭建热插拔式web框架(实现原理)

    第一节:我们为什么需要一个热插拔式的web框架? 模块之间独立开发 假设我们要做一个后台管理系统,其中包括“用户活跃度”.“产品管理”."账单管理"等模块.每个模块中有自己的业务特 ...