前言:本博文重在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. Python_查询手机供应商

    def Seach_Phone_Nmuber_operator(): CN_mobile = [134,135,136,137,138,139,150,151,152,157,158,159,182, ...

  2. MyEclipse部署web项目到Tomcat出现An internal error occurred during: "Launching on Tomcat 7.x"的问题

    如果出现了上述的错误按照如下的3个步骤解决:1.首先关闭MyEclipse工作空间.2.然后删除工作空间下的文件."MyEclipse10\workspace.metadata.plugin ...

  3. ASP.NET MVC移动M站建设-使用51Degree 移动设备的识别

    上一篇,介绍了移动M站的建设.说的很简单.觉得好像也没把M站给讲清楚.估计是对移动M站 认识还不够深刻吧.这里,在讲一讲51Degree 这个组件. 51degrees 号称是目前最快.最准确的设备检 ...

  4. 红米2A高配刷机记录

    2014816 机型:红米2A高配版 设备型号:2014816 CPU:高通 线刷:fastboot平台 http://192.168.7.118/MesReports/Reports/Cutting ...

  5. 项目中运行报错: Loading XML bean definitions from class path resource [applicationContext.xml]

    记录一下: org.springframework.context.support.AbstractApplicationContext prepareRefresh Refreshing org.s ...

  6. iPhone屏幕尺寸/launch尺寸/icon尺寸

    屏幕尺寸 6p/6sp     414 X 736 6/6s         375 X 667 5/5s         320 X 568  4/4s         320 X 480   la ...

  7. SQL Server群集如何在线检测

    SQL Server群集知识介绍 Windows群集安装 基于iSCSI的SQL Server 2012群集测试 前言 群集的检测是调用dll资源,例如对于共享存储,ip,网络名称与DTC 这类Win ...

  8. 内核编译报错Fedora20(友善)

    首先说明我的宿主机环境:Fedora20 64位,开发板是友善Smart210(S5PV210——Cotex-A8)!!!马上入题! 按照开发板的用户手册来编译内核,一直报这个错误:/opt/Frie ...

  9. frp配置

    frps配置 --------------------------------------------------------------------------------------------- ...

  10. Func

    Func<List<int>, string> getStr = (list) => { var returnStr = ""; if (list.A ...